diff --git a/cmd/ig.go b/cmd/ig.go new file mode 100644 index 0000000..3b27143 --- /dev/null +++ b/cmd/ig.go @@ -0,0 +1,842 @@ +package cmd + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/Facets-cloud/praxis-cli/internal/credentials" + "github.com/Facets-cloud/praxis-cli/internal/exitcode" + "github.com/Facets-cloud/praxis-cli/internal/igcatalog" + "github.com/Facets-cloud/praxis-cli/internal/render" + "github.com/spf13/cobra" +) + +// `praxis ig` is the ONLY client of the ig catalog server. Repo CI, +// laptops, and humans call this CLI; the `ig` binary itself never learns +// servers exist — it reads the filesystem that `praxis ig sync` +// materializes under $IG_HOME. praxis fetches; ig reads. This file is the +// half that talks to the network so that ig never has to. +// +// The noun is `ig` (not `catalog`) because praxis already uses "catalog" +// to mean the skill catalog (`praxis login` syncs the org skill catalog). + +const igDefaultHome = ".ig" + +// errBundleMismatch marks a downloaded bundle whose bytes don't hash to +// the server-supplied digest. sync refuses to replace the live tree and +// exits non-zero — CI depends on this. +var errBundleMismatch = errors.New("bundle digest mismatch") + +// errBundleContract marks a downloaded bundle whose extracted tree violates +// the archive contract: metadata.json MUST sit at the tree root. sync +// refuses to swap such a tree into place and exits non-zero. Silently +// tolerating a malformed bundle is exactly what once materialized a broken, +// double-nested catalog (projects///metadata.json) that `ig status` +// then reported as NO_METADATA while praxis claimed success. +var errBundleContract = errors.New("bundle contract violation") + +// maxMemberFileBytes bounds a single extracted file so a malformed or +// hostile bundle can't exhaust the disk. Catalog members are JSON graphs; +// this is a generous ceiling, not a normal size. +const maxMemberFileBytes = 1 << 30 // 1 GiB + +var ( + igJSON bool + igProfile string + + igSyncAll bool + + igPublishCatalog string + igPublishMember string + igPublishGit string + igPublishSha string + + igClaimsGit string + + igManifestCatalog string + igManifestOut string +) + +// syncState is the .sync.json handshake praxis WRITES and ig READS. ig +// never writes one; it only reads `refresh` (echoed verbatim as the fix on +// its CATALOG_SYNC_STALE issue) and `from` (opaque provenance it displays +// but never parses). +type syncState struct { + SyncedAt string `json:"synced_at"` + Digest string `json:"digest"` + From string `json:"from"` + Refresh string `json:"refresh"` +} + +// memberMeta is the optional /member//member-meta.json that +// carries a member's canonical git URL and commit sha, so `publish` +// doesn't need --git/--sha flags when CI already wrote the meta. +type memberMeta struct { + Git string `json:"git"` + Sha string `json:"sha"` +} + +// gitHeadSHA is a seam (tests stub it) that returns the HEAD sha of the +// working tree a manifest file came from. Shelling out to `git` is fine — +// only shelling out to `ig` is forbidden. +var gitHeadSHA = func(dir string) (string, error) { + cmd := exec.Command("git", "-C", dir, "rev-parse", "HEAD") + out, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + +// nowFn is a seam for the sync timestamp. +var nowFn = time.Now + +// --- $IG_HOME layout --------------------------------------------------- + +func igHome() (string, error) { + if h := os.Getenv("IG_HOME"); h != "" { + return h, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, igDefaultHome), nil +} + +func catalogDir(home, catalog string) string { + return filepath.Join(home, "projects", catalog) +} + +func readSyncState(dir string) (syncState, bool) { + data, err := os.ReadFile(filepath.Join(dir, ".sync.json")) + if err != nil { + return syncState{}, false + } + var s syncState + if json.Unmarshal(data, &s) != nil { + return syncState{}, false + } + return s, true +} + +func writeSyncState(dir string, s syncState) error { + data, err := json.MarshalIndent(s, "", " ") + if err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, ".sync.json"), append(data, '\n'), 0o644) +} + +// syncedCatalogs lists the catalogs already materialized under $IG_HOME +// (those with a .sync.json), sorted, for `status` with no argument. +func syncedCatalogs(home string) []string { + entries, err := os.ReadDir(filepath.Join(home, "projects")) + if err != nil { + return nil + } + var out []string + for _, e := range entries { + if !e.IsDir() { + continue + } + if _, ok := readSyncState(filepath.Join(home, "projects", e.Name())); ok { + out = append(out, e.Name()) + } + } + sort.Strings(out) + return out +} + +// --- the linchpin: refresh composition --------------------------------- + +// composeRefresh is the exact command that reproduces this sync. ig echoes +// it verbatim as the fix on CATALOG_SYNC_STALE — ig composes nothing, so +// this MUST include `--profile

` whenever the active profile is not the +// default. That is what keeps ig ignorant of praxis: it relays a string it +// was handed rather than building one. +func composeRefresh(catalog, profileName string) string { + cmd := "praxis ig sync " + catalog + if profileName != "" && profileName != credentials.DefaultProfileName { + cmd += " --profile " + profileName + } + return cmd +} + +// --- sync core (testable; never calls os.Exit) ------------------------- + +// syncOne downloads a catalog's bundle (conditional on the local digest), +// verifies it, and atomically materializes it under $IG_HOME, writing +// .sync.json beside it. Returns upToDate=true when the server answered 304 +// (no re-extract). Never calls os.Exit — the cmd layer maps err to a code. +func syncOne(active credentials.Active, catalog string) (upToDate bool, err error) { + home, err := igHome() + if err != nil { + return false, err + } + dir := catalogDir(home, catalog) + local, _ := readSyncState(dir) + + body, etag, notModified, err := igcatalog.DownloadBundle( + active.Profile.URL, active.Profile.Token, catalog, local.Digest) + if err != nil { + return false, err + } + if notModified { + return true, nil + } + if err := verifyDigest(body, etag); err != nil { + return false, err + } + + if err := os.MkdirAll(home, 0o755); err != nil { + return false, err + } + tmp, err := os.MkdirTemp(home, ".sync-"+sanitizeTemp(catalog)+"-*") + if err != nil { + return false, err + } + committed := false + defer func() { + if !committed { + _ = os.RemoveAll(tmp) + } + }() + + if err := extractTarGz(body, tmp); err != nil { + return false, err + } + // Enforce the archive contract on the freshly-extracted tree BEFORE any + // swap: metadata.json MUST sit at the root. A malformed bundle fails + // here, the deferred cleanup drops the temp dir, and the live tree at + // `dir` is never touched. + if err := validateBundleTree(tmp, catalog); err != nil { + return false, err + } + state := syncState{ + SyncedAt: nowFn().UTC().Format(time.RFC3339), + Digest: etag, + From: fmt.Sprintf("praxis@%s:%s@%s", active.Name, catalog, etag), + Refresh: composeRefresh(catalog, active.Name), + } + if err := writeSyncState(tmp, state); err != nil { + return false, err + } + if err := swapDir(tmp, dir); err != nil { + return false, err + } + committed = true + return false, nil +} + +// verifyDigest confirms the downloaded bytes hash to the server-supplied +// digest. Only sha256: digests are verifiable; any other opaque validator +// is trusted (the server is authoritative for its own ETag semantics). +func verifyDigest(body []byte, etag string) error { + if !strings.HasPrefix(etag, "sha256:") { + return nil + } + want := strings.TrimPrefix(etag, "sha256:") + sum := sha256.Sum256(body) + got := hex.EncodeToString(sum[:]) + if !strings.EqualFold(got, want) { + return fmt.Errorf("%w: server said %s but downloaded bytes hash to sha256:%s", errBundleMismatch, etag, got) + } + return nil +} + +// swapDir atomically moves the freshly-extracted tmp tree into place. The +// previous tree is renamed aside first and restored if the final rename +// fails, so a crash mid-swap never leaves a half-extracted catalog and +// never loses the old one. +func swapDir(tmp, dir string) error { + if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil { + return err + } + var backup string + if _, err := os.Stat(dir); err == nil { + backup = dir + ".bak-" + fmt.Sprint(nowFn().UnixNano()) + if err := os.Rename(dir, backup); err != nil { + return err + } + } + if err := os.Rename(tmp, dir); err != nil { + if backup != "" { + _ = os.Rename(backup, dir) // restore the previous tree + } + return err + } + if backup != "" { + _ = os.RemoveAll(backup) + } + return nil +} + +// extractTarGz unpacks a gzipped tarball into dest, guarding against +// path-traversal ("zip slip") and oversized entries. +func extractTarGz(data []byte, dest string) error { + gz, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("gunzip bundle: %w", err) + } + defer func() { _ = gz.Close() }() + tr := tar.NewReader(gz) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("read bundle tar: %w", err) + } + clean := filepath.Clean(hdr.Name) + if clean == "." || clean == "" { + continue + } + target := filepath.Join(dest, clean) + if !withinDir(dest, target) { + return fmt.Errorf("bundle entry %q escapes the destination directory", hdr.Name) + } + switch hdr.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0o755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + if err := writeTarFile(tr, target, hdr); err != nil { + return err + } + default: + // Skip symlinks, devices, etc. — a catalog bundle is plain files. + } + } + return nil +} + +func writeTarFile(tr *tar.Reader, target string, hdr *tar.Header) error { + mode := os.FileMode(hdr.Mode).Perm() + if mode == 0 { + mode = 0o644 + } + f, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) + if err != nil { + return err + } + n, err := io.Copy(f, io.LimitReader(tr, maxMemberFileBytes+1)) + if cerr := f.Close(); err == nil { + err = cerr + } + if err != nil { + return err + } + if n > maxMemberFileBytes { + return fmt.Errorf("bundle entry %q exceeds %d bytes", hdr.Name, maxMemberFileBytes) + } + return nil +} + +func withinDir(base, target string) bool { + rel, err := filepath.Rel(base, target) + if err != nil { + return false + } + return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))) +} + +// validateBundleTree enforces the ig archive contract on a freshly-extracted +// tree, run BEFORE the tree is swapped into place. The bundle must carry the +// catalog directory's *contents* — metadata.json at the archive root, plus +// member//graphify-out/graph.json and catalog/graphify-out/graph.json — so +// a valid tree always has metadata.json at its root. +// +// The failure we hit in production: the server emitted entries prefixed with +// the catalog name (`/metadata.json`, ...), which extracts to +// `/metadata.json` and, once swapped in, becomes the double-nested +// projects///metadata.json that `ig status` calls +// NO_METADATA. We diagnose that exact shape specifically. +// +// We deliberately do NOT strip the prefix to "fix it up": silently tolerating +// a malformed bundle is precisely what let the broken tree ship and would +// hide the next server regression. Fail loudly instead. +func validateBundleTree(dir, catalog string) error { + if isRegularFile(filepath.Join(dir, "metadata.json")) { + return nil + } + // The exact shape of the bug: the tree is rooted at a single directory + // named after the catalog, with metadata.json one level down. + if isRegularFile(filepath.Join(dir, catalog, "metadata.json")) { + return fmt.Errorf("%w: bundle for catalog %q has no metadata.json at its root; it appears to be rooted at %s/ instead, so the server is emitting a catalog-prefixed archive. Expected the catalog directory's contents (metadata.json at the archive root), not a %s/ directory", + errBundleContract, catalog, catalog, catalog) + } + return fmt.Errorf("%w: bundle for catalog %q has no metadata.json at its root; the archive does not match the ig catalog contract", + errBundleContract, catalog) +} + +// isRegularFile reports whether path is an existing regular file (not a +// directory or other node). +func isRegularFile(path string) bool { + fi, err := os.Stat(path) + return err == nil && fi.Mode().IsRegular() +} + +// sanitizeTemp keeps the temp-dir suffix filesystem-safe for catalog names +// that contain slashes or other separators. +func sanitizeTemp(s string) string { + return strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_': + return r + default: + return '_' + } + }, s) +} + +// --- status core (no bundle download) ---------------------------------- + +// statusOne compares the local .sync.json digest against the server's +// current version WITHOUT downloading the bundle. state is one of +// "up to date" | "stale" | "not synced". +func statusOne(active credentials.Active, catalog string) (state, serverVersion, localDigest string, err error) { + home, err := igHome() + if err != nil { + return "", "", "", err + } + local, synced := readSyncState(catalogDir(home, catalog)) + c, err := igcatalog.GetCatalog(active.Profile.URL, active.Profile.Token, catalog) + if err != nil { + return "", "", "", err + } + switch { + case !synced: + state = "not synced" + case local.Digest == c.Version: + state = "up to date" + default: + state = "stale" + } + return state, c.Version, local.Digest, nil +} + +// --- auth resolution honoring --profile -------------------------------- + +// activeOrAuthExitProfile resolves the credentials profile (honoring the +// --profile flag) or exits with the auth code. Variant of memory.go's +// activeOrAuthExit that threads the flag so a non-default profile is +// selectable — and reproducible in .sync.json's refresh. +func activeOrAuthExitProfile(out io.Writer, profileFlag string) credentials.Active { + active, err := credentials.ResolveActive(profileFlag) + if err != nil { + render.PrintError(out, true, err.Error(), "could not load credentials", exitcode.Error) + os.Exit(exitcode.Error) + } + if !active.Loaded || active.Profile.Token == "" { + render.PrintError(out, true, + fmt.Sprintf("no credentials for profile %q", active.Name), + "run `praxis login` (or `praxis login --profile "+active.Name+"`)", + exitcode.Auth) + os.Exit(exitcode.Auth) + } + return active +} + +// --- command tree ------------------------------------------------------ + +func init() { + igCmd.PersistentFlags().BoolVar(&igJSON, "json", false, "JSON output (default when stdout is non-TTY)") + igCmd.PersistentFlags().StringVar(&igProfile, "profile", "", "credentials profile to use (defaults to the active profile)") + + igSyncCmd.Flags().BoolVar(&igSyncAll, "all", false, "sync every catalog in the org") + + igPublishCmd.Flags().StringVar(&igPublishCatalog, "catalog", "", "target catalog (required)") + igPublishCmd.Flags().StringVar(&igPublishMember, "member", "", "member name (required)") + igPublishCmd.Flags().StringVar(&igPublishGit, "git", "", "member's canonical git URL (overrides member-meta.json)") + igPublishCmd.Flags().StringVar(&igPublishSha, "sha", "", "member's commit sha (overrides member-meta.json)") + + igClaimsCmd.Flags().StringVar(&igClaimsGit, "git", "", "canonical git URL to look up (required)") + + igManifestPushCmd.Flags().StringVar(&igManifestCatalog, "catalog", "", "target catalog (required)") + igManifestPullCmd.Flags().StringVar(&igManifestOut, "out", "", "write the served manifest here (default: stdout)") + + igManifestCmd.AddCommand(igManifestPushCmd) + igManifestCmd.AddCommand(igManifestPullCmd) + + igCmd.AddCommand(igListCmd) + igCmd.AddCommand(igSyncCmd) + igCmd.AddCommand(igStatusCmd) + igCmd.AddCommand(igPublishCmd) + igCmd.AddCommand(igClaimsCmd) + igCmd.AddCommand(igManifestCmd) + rootCmd.AddCommand(igCmd) +} + +var igCmd = &cobra.Command{ + Use: "ig", + Short: "Fetch and publish ig catalogs (the only client of the ig catalog server)", + Long: `ig builds a portable "catalog" — a graph-of-graphs over a project's code +repos plus its Facets infra. praxis is the ONLY network client of the ig +catalog server: it downloads catalogs and writes them to the filesystem; +the ` + "`ig`" + ` binary reads that filesystem and never talks to a server. + + praxis ig list the org's catalogs + praxis ig sync | --all download a catalog into $IG_HOME + praxis ig status [] is my local copy up to date? + praxis ig publish

--catalog --member upload one member's graph + praxis ig claims --git which catalogs claim this repo + praxis ig manifest push --catalog + praxis ig manifest pull [--out ] + +Catalogs materialize under $IG_HOME (default ~/.ig) at projects//, +with a .sync.json handshake beside each so ig knows how to refresh.`, +} + +// --- list -------------------------------------------------------------- + +var igListCmd = &cobra.Command{ + Use: "list", + Short: "List the org's catalogs (name, version, built_at, member count)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + asJSON := render.UseJSON(igJSON, false, out) + active := activeOrAuthExitProfile(out, igProfile) + + cats, err := igcatalog.ListCatalogs(active.Profile.URL, active.Profile.Token) + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + if cats == nil { + cats = []igcatalog.Catalog{} + } + if asJSON { + return render.JSON(out, cats) + } + printCatalogsPretty(out, cats) + return nil + }, +} + +// --- sync -------------------------------------------------------------- + +var igSyncCmd = &cobra.Command{ + Use: "sync []", + Short: "Download a catalog's bundle into $IG_HOME and write .sync.json", + Long: `Materializes a catalog under $IG_HOME/projects// and writes a +.sync.json handshake beside it. Uses If-None-Match with the local digest so +an unchanged catalog is a cheap 304 and no re-download. Extraction is atomic +(temp dir + rename), and a digest mismatch fails loudly without touching the +live tree.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + asJSON := render.UseJSON(igJSON, false, out) + if igSyncAll && len(args) > 0 { + usageExit(out, "pass either or --all, not both", "") + } + if !igSyncAll && len(args) != 1 { + usageExit(out, "sync requires exactly one (or --all)", "praxis ig sync ") + } + active := activeOrAuthExitProfile(out, igProfile) + + targets := args + if igSyncAll { + cats, err := igcatalog.ListCatalogs(active.Profile.URL, active.Profile.Token) + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + for _, c := range cats { + targets = append(targets, c.Name) + } + } + + results := make([]map[string]string, 0, len(targets)) + for _, c := range targets { + upToDate, err := syncOne(active, c) + if err != nil { + if errors.Is(err, errBundleMismatch) { + render.PrintError(out, asJSON, fmt.Sprintf("sync %q: %v", c, err), + "refusing to replace the local catalog on a digest mismatch", exitcode.Error) + os.Exit(exitcode.Error) + } + if errors.Is(err, errBundleContract) { + render.PrintError(out, asJSON, fmt.Sprintf("sync %q: %v", c, err), + "the server sent a malformed catalog bundle; the local catalog was left untouched", exitcode.Error) + os.Exit(exitcode.Error) + } + return reportHTTPErr(out, active.Name, err) + } + status := "synced" + if upToDate { + status = "up to date" + } + results = append(results, map[string]string{"catalog": c, "status": status}) + } + if asJSON { + return render.JSON(out, results) + } + for _, r := range results { + fmt.Fprintf(out, "%s: %s\n", r["catalog"], r["status"]) + } + return nil + }, +} + +// --- status ------------------------------------------------------------ + +var igStatusCmd = &cobra.Command{ + Use: "status []", + Short: "Compare the local catalog against the server WITHOUT downloading", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + asJSON := render.UseJSON(igJSON, false, out) + active := activeOrAuthExitProfile(out, igProfile) + + var targets []string + if len(args) == 1 { + targets = []string{args[0]} + } else { + home, err := igHome() + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + targets = syncedCatalogs(home) + } + + results := make([]map[string]string, 0, len(targets)) + for _, c := range targets { + state, serverVersion, localDigest, err := statusOne(active, c) + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + results = append(results, map[string]string{ + "catalog": c, "status": state, + "server_version": serverVersion, "local_digest": localDigest, + }) + } + if asJSON { + return render.JSON(out, results) + } + if len(results) == 0 { + fmt.Fprintln(out, "(no catalogs synced)") + return nil + } + for _, r := range results { + if r["status"] == "stale" { + fmt.Fprintf(out, "%s: stale (server has %s)\n", r["catalog"], r["server_version"]) + } else { + fmt.Fprintf(out, "%s: %s\n", r["catalog"], r["status"]) + } + } + return nil + }, +} + +// --- publish ----------------------------------------------------------- + +var igPublishCmd = &cobra.Command{ + Use: "publish --catalog --member ", + Short: "Upload one member's graph.json (gzipped) to a catalog", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + asJSON := render.UseJSON(igJSON, false, out) + if igPublishCatalog == "" || igPublishMember == "" { + usageExit(out, "both --catalog and --member are required", "") + } + dir := args[0] + memberDir := filepath.Join(dir, "member", igPublishMember) + graphPath := filepath.Join(memberDir, "graphify-out", "graph.json") + raw, err := os.ReadFile(graphPath) + if err != nil { + usageExit(out, fmt.Sprintf("read %s: %v", graphPath, err), + "expected /member//graphify-out/graph.json") + } + + git, sha := igPublishGit, igPublishSha + if meta, ok := readMemberMeta(memberDir); ok { + if git == "" { + git = meta.Git + } + if sha == "" { + sha = meta.Sha + } + } + if git == "" || sha == "" { + usageExit(out, "member git URL and sha are required", + "provide --git/--sha, or a member-meta.json with {git, sha}") + } + + active := activeOrAuthExitProfile(out, igProfile) + + gz := gzipBytes(raw) + if err := igcatalog.PublishMember(active.Profile.URL, active.Profile.Token, + igPublishCatalog, igPublishMember, gz, git, sha); err != nil { + return reportHTTPErr(out, active.Name, err) + } + result := map[string]any{ + "catalog": igPublishCatalog, "member": igPublishMember, + "git": git, "sha": sha, "bytes": len(gz), "status": "published", + } + if asJSON { + return render.JSON(out, result) + } + fmt.Fprintf(out, "published %s/%s (%d bytes, sha %s)\n", igPublishCatalog, igPublishMember, len(gz), sha) + return nil + }, +} + +func readMemberMeta(memberDir string) (memberMeta, bool) { + data, err := os.ReadFile(filepath.Join(memberDir, "member-meta.json")) + if err != nil { + return memberMeta{}, false + } + var m memberMeta + if json.Unmarshal(data, &m) != nil { + return memberMeta{}, false + } + return m, true +} + +func gzipBytes(data []byte) []byte { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + _, _ = zw.Write(data) + _ = zw.Close() + return buf.Bytes() +} + +// --- claims ------------------------------------------------------------ + +var igClaimsCmd = &cobra.Command{ + Use: "claims --git ", + Short: "Print the catalogs claiming a repo, one name per line (for CI loops)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + if igClaimsGit == "" { + usageExit(out, "--git is required", "praxis ig claims --git https://github.com/org/repo.git") + } + active := activeOrAuthExitProfile(out, igProfile) + + names, err := igcatalog.Claims(active.Profile.URL, active.Profile.Token, igClaimsGit) + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + // Default is newline-delimited so `for c in $(praxis ig claims …)` + // works when piped; --json gives an array. (This verb intentionally + // does NOT auto-JSON on a pipe — CI loops want bare lines.) + if igJSON { + if names == nil { + names = []string{} + } + return render.JSON(out, names) + } + for _, n := range names { + fmt.Fprintln(out, n) + } + return nil + }, +} + +// --- manifest ---------------------------------------------------------- + +var igManifestCmd = &cobra.Command{ + Use: "manifest", + Short: "Push or pull a catalog's manifest text", +} + +var igManifestPushCmd = &cobra.Command{ + Use: "push --catalog ", + Short: "Upload a manifest, stamping pushed_by, pushed_at, and git_sha", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + asJSON := render.UseJSON(igJSON, false, out) + if igManifestCatalog == "" { + usageExit(out, "--catalog is required", "praxis ig manifest push --catalog ") + } + file := args[0] + content, err := os.ReadFile(file) + if err != nil { + usageExit(out, fmt.Sprintf("read %s: %v", file, err), "") + } + + // git_sha is best-effort: a manifest may come from a non-git dir. + gitSHA, _ := gitHeadSHA(filepath.Dir(file)) + + active := activeOrAuthExitProfile(out, igProfile) + + m := igcatalog.Manifest{ + Content: string(content), + PushedBy: active.Profile.Username, + PushedAt: nowFn().UTC().Format(time.RFC3339), + GitSHA: gitSHA, + } + if err := igcatalog.ManifestPush(active.Profile.URL, active.Profile.Token, igManifestCatalog, m); err != nil { + return reportHTTPErr(out, active.Name, err) + } + result := map[string]string{ + "catalog": igManifestCatalog, "pushed_by": m.PushedBy, + "pushed_at": m.PushedAt, "git_sha": m.GitSHA, "status": "pushed", + } + if asJSON { + return render.JSON(out, result) + } + fmt.Fprintf(out, "pushed manifest for %s (git_sha %s)\n", igManifestCatalog, m.GitSHA) + return nil + }, +} + +var igManifestPullCmd = &cobra.Command{ + Use: "pull [--out ]", + Short: "Fetch the served manifest (verbatim) to diff against a local copy", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + active := activeOrAuthExitProfile(out, igProfile) + + m, err := igcatalog.ManifestPull(active.Profile.URL, active.Profile.Token, args[0]) + if err != nil { + return reportHTTPErr(out, active.Name, err) + } + if igManifestOut != "" { + if err := os.WriteFile(igManifestOut, []byte(m.Content), 0o644); err != nil { + render.PrintError(out, true, fmt.Sprintf("write %s: %v", igManifestOut, err), "", exitcode.Error) + os.Exit(exitcode.Error) + } + fmt.Fprintf(out, "wrote %s (%d bytes)\n", igManifestOut, len(m.Content)) + return nil + } + _, _ = out.Write([]byte(m.Content)) + return nil + }, +} + +// --- pretty printers --------------------------------------------------- + +func printCatalogsPretty(out io.Writer, cats []igcatalog.Catalog) { + if len(cats) == 0 { + fmt.Fprintln(out, "(no catalogs)") + return + } + for _, c := range cats { + fmt.Fprintf(out, "%s %s %s (%d members)\n", c.Name, c.Version, c.BuiltAt, len(c.Members)) + } +} diff --git a/cmd/ig_test.go b/cmd/ig_test.go new file mode 100644 index 0000000..c21c473 --- /dev/null +++ b/cmd/ig_test.go @@ -0,0 +1,775 @@ +package cmd + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Facets-cloud/praxis-cli/internal/credentials" + "github.com/Facets-cloud/praxis-cli/internal/igcatalog" +) + +// resetIgFlags returns the module-level cobra-bound vars to defaults +// between tests — cobra holds these globals across RunE calls. +func resetIgFlags() { + igJSON = false + igProfile = "" + igSyncAll = false + igPublishCatalog = "" + igPublishMember = "" + igPublishGit = "" + igPublishSha = "" + igClaimsGit = "" + igManifestCatalog = "" + igManifestOut = "" +} + +func seedIgProfile(t *testing.T, name string) { + t.Helper() + t.Setenv("HOME", t.TempDir()) + if err := credentials.Put(name, credentials.Profile{ + URL: "https://x.test", Username: "u@x.com", Token: "sk_test_T", + }); err != nil { + t.Fatal(err) + } + if name != "default" { + if err := credentials.SetActive(name); err != nil { + t.Fatal(err) + } + } +} + +func testActive() credentials.Active { + return credentials.Active{ + Name: "default", + Profile: credentials.Profile{URL: "https://x.test", Token: "tok", Username: "u@x.com"}, + Loaded: true, + } +} + +// makeTarGz builds a gzipped tarball of name→content regular files. +func makeTarGz(t *testing.T, files map[string]string) []byte { + t.Helper() + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + for name, content := range files { + if err := tw.WriteHeader(&tar.Header{ + Name: name, Mode: 0o644, Size: int64(len(content)), Typeflag: tar.TypeReg, + }); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func sha256Etag(data []byte) string { + sum := sha256.Sum256(data) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// --- refresh composition (the linchpin) -------------------------------- + +func TestComposeRefresh_ProfileFlagOnlyForNonDefault(t *testing.T) { + if got := composeRefresh("payments", "default"); got != "praxis ig sync payments" { + t.Errorf("default profile: got %q; want no --profile", got) + } + if got := composeRefresh("payments", "acme"); got != "praxis ig sync payments --profile acme" { + t.Errorf("non-default profile: got %q", got) + } +} + +// --- sync: writes .sync.json with all four fields ---------------------- + +func TestIgSync_WritesSyncStateAndComposesRefresh(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + tarball := makeTarGz(t, map[string]string{ + "metadata.json": `{"catalog":"payments"}`, + "graph.json": `{"root":true}`, + "member/api/graph.json": `{"m":"api"}`, + }) + etag := sha256Etag(tarball) + + var gotINM string + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(baseURL, token, catalog, inm string) ([]byte, string, bool, error) { + gotINM = inm + if baseURL != "https://x.test" || token != "tok" { + t.Errorf("auth threading: url=%q token=%q", baseURL, token) + } + if catalog != "payments" { + t.Errorf("catalog = %q", catalog) + } + return tarball, etag, false, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + active := credentials.Active{ + Name: "acme", + Profile: credentials.Profile{URL: "https://x.test", Token: "tok", Username: "u@x.com"}, + Loaded: true, + } + upToDate, err := syncOne(active, "payments") + if err != nil { + t.Fatalf("syncOne: %v", err) + } + if upToDate { + t.Error("first sync should not report up-to-date") + } + if gotINM != "" { + t.Errorf("first sync should send empty If-None-Match, got %q", gotINM) + } + + dir := filepath.Join(igHomeDir, "projects", "payments") + if b, err := os.ReadFile(filepath.Join(dir, "graph.json")); err != nil || string(b) != `{"root":true}` { + t.Errorf("graph.json = %q err=%v", b, err) + } + if b, err := os.ReadFile(filepath.Join(dir, "member", "api", "graph.json")); err != nil || string(b) != `{"m":"api"}` { + t.Errorf("member graph.json = %q err=%v", b, err) + } + + st, ok := readSyncState(dir) + if !ok { + t.Fatal(".sync.json was not written") + } + if st.Digest != etag { + t.Errorf("digest = %q; want %q", st.Digest, etag) + } + if st.From != "praxis@acme:payments@"+etag { + t.Errorf("from = %q; want praxis@acme:payments@%s", st.From, etag) + } + if st.Refresh != "praxis ig sync payments --profile acme" { + t.Errorf("refresh = %q; want the exact reproducing command with --profile acme", st.Refresh) + } + if st.SyncedAt == "" { + t.Error("synced_at empty") + } + if _, e := time.Parse(time.RFC3339, st.SyncedAt); e != nil { + t.Errorf("synced_at %q not RFC3339: %v", st.SyncedAt, e) + } +} + +// --- sync: 304 keeps the tree, sends If-None-Match --------------------- + +func TestIgSync_NotModifiedIsCheapNoOp(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + dir := filepath.Join(igHomeDir, "projects", "payments") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "marker.txt"), []byte("KEEP-ME"), 0o644); err != nil { + t.Fatal(err) + } + prev := syncState{ + SyncedAt: "2026-07-01T00:00:00Z", Digest: "sha256:old", + From: "praxis@default:payments@sha256:old", Refresh: "praxis ig sync payments", + } + if err := writeSyncState(dir, prev); err != nil { + t.Fatal(err) + } + + var gotINM string + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, inm string) ([]byte, string, bool, error) { + gotINM = inm + return nil, "sha256:old", true, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + upToDate, err := syncOne(testActive(), "payments") + if err != nil { + t.Fatalf("syncOne: %v", err) + } + if !upToDate { + t.Error("304 must report up-to-date") + } + if gotINM != "sha256:old" { + t.Errorf("If-None-Match = %q; want the local digest sha256:old", gotINM) + } + if b, err := os.ReadFile(filepath.Join(dir, "marker.txt")); err != nil || string(b) != "KEEP-ME" { + t.Errorf("304 must not re-extract; marker = %q err=%v", b, err) + } + st, _ := readSyncState(dir) + if st.SyncedAt != "2026-07-01T00:00:00Z" { + t.Errorf("304 must not rewrite .sync.json; got %+v", st) + } +} + +// --- sync: ETag is quoted HTTP syntax, not catalog data (RFC 9110) ----- +// +// These two hit a REAL httptest.Server rather than stubbing +// igcatalog.DownloadBundle, so the actual net/http header round-trip is +// exercised end to end: a real server's `ETag` header is a *quoted* +// string (RFC 9110 §8.8.3), e.g. `ETag: "v1.2.3"`. syncOne must store the +// bare tag — no quotes — in .sync.json's digest/from fields, since a +// human reads `from` in ig's provenance footer and quotes there are HTTP +// syntax leaking into displayed data. + +func TestIgSync_StripsQuotedETagIntoDigestAndFrom(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + tarball := makeTarGz(t, map[string]string{ + "metadata.json": `{"catalog":"payments"}`, + "graph.json": `{"root":true}`, + }) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", `"v1.2.3"`) + _, _ = w.Write(tarball) + })) + defer srv.Close() + + active := credentials.Active{ + Name: "acme", + Profile: credentials.Profile{URL: srv.URL, Token: "tok", Username: "u@x.com"}, + Loaded: true, + } + if _, err := syncOne(active, "payments"); err != nil { + t.Fatalf("syncOne: %v", err) + } + + dir := filepath.Join(igHomeDir, "projects", "payments") + st, ok := readSyncState(dir) + if !ok { + t.Fatal(".sync.json was not written") + } + if st.Digest != "v1.2.3" { + t.Errorf("digest = %q; want the bare tag v1.2.3 with no quotes", st.Digest) + } + if st.From != "praxis@acme:payments@v1.2.3" { + t.Errorf("from = %q; want praxis@acme:payments@v1.2.3 with no quotes", st.From) + } +} + +// TestIgSync_ReSyncSendsQuotedIfNoneMatchAndTreats304AsUpToDate is the +// send-side half: once the stored digest is bare (post-fix), re-syncing +// must re-quote it before sending If-None-Match — a bare unquoted token +// is invalid HTTP syntax and a spec-compliant server need not honor it — +// and a resulting 304 must still be treated as "already current" with no +// re-extract. +func TestIgSync_ReSyncSendsQuotedIfNoneMatchAndTreats304AsUpToDate(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + dir := filepath.Join(igHomeDir, "projects", "payments") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "marker.txt"), []byte("KEEP-ME"), 0o644); err != nil { + t.Fatal(err) + } + // A catalog already synced by the FIXED code: the stored digest is + // bare, no quotes. + if err := writeSyncState(dir, syncState{ + SyncedAt: "2026-07-01T00:00:00Z", Digest: "v1.2.3", + From: "praxis@default:payments@v1.2.3", Refresh: "praxis ig sync payments", + }); err != nil { + t.Fatal(err) + } + + var gotINM string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotINM = r.Header.Get("If-None-Match") + w.Header().Set("ETag", `"v1.2.3"`) + w.WriteHeader(http.StatusNotModified) + })) + defer srv.Close() + + active := credentials.Active{ + Name: "default", + Profile: credentials.Profile{URL: srv.URL, Token: "tok", Username: "u@x.com"}, + Loaded: true, + } + upToDate, err := syncOne(active, "payments") + if err != nil { + t.Fatalf("syncOne: %v", err) + } + if !upToDate { + t.Error("304 must report up-to-date") + } + if gotINM != `"v1.2.3"` { + t.Errorf("If-None-Match sent = %q; want the properly quoted validator %q", gotINM, `"v1.2.3"`) + } + if b, err := os.ReadFile(filepath.Join(dir, "marker.txt")); err != nil || string(b) != "KEEP-ME" { + t.Errorf("304 must not re-extract; marker = %q err=%v", b, err) + } + st, _ := readSyncState(dir) + if st.SyncedAt != "2026-07-01T00:00:00Z" { + t.Errorf("304 must not rewrite .sync.json; got %+v", st) + } +} + +// --- sync: digest mismatch fails loudly, tree intact ------------------- + +func TestIgSync_DigestMismatchFailsAndKeepsTree(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + dir := filepath.Join(igHomeDir, "projects", "payments") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "marker.txt"), []byte("KEEP-ME"), 0o644); err != nil { + t.Fatal(err) + } + if err := writeSyncState(dir, syncState{Digest: "sha256:old"}); err != nil { + t.Fatal(err) + } + + tarball := makeTarGz(t, map[string]string{"graph.json": "x"}) + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, _ string) ([]byte, string, bool, error) { + return tarball, "sha256:0000thisisthewronghash0000", false, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + if _, err := syncOne(testActive(), "payments"); err == nil || !strings.Contains(err.Error(), "mismatch") { + t.Fatalf("expected digest mismatch error, got %v", err) + } + if b, err := os.ReadFile(filepath.Join(dir, "marker.txt")); err != nil || string(b) != "KEEP-ME" { + t.Errorf("mismatch must not replace live tree; marker = %q err=%v", b, err) + } + st, _ := readSyncState(dir) + if st.Digest != "sha256:old" { + t.Errorf("mismatch must not rewrite .sync.json; got %+v", st) + } +} + +// --- sync: a good 200 atomically replaces the previous tree ------------ + +func TestIgSync_ReplacesExistingTreeCleanly(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + dir := filepath.Join(igHomeDir, "projects", "payments") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "OLD.txt"), []byte("old"), 0o644); err != nil { + t.Fatal(err) + } + if err := writeSyncState(dir, syncState{Digest: "sha256:old"}); err != nil { + t.Fatal(err) + } + + tarball := makeTarGz(t, map[string]string{"metadata.json": `{"catalog":"payments"}`, "NEW.txt": "new"}) + etag := sha256Etag(tarball) + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, inm string) ([]byte, string, bool, error) { + if inm != "sha256:old" { + t.Errorf("If-None-Match = %q; want sha256:old", inm) + } + return tarball, etag, false, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + if _, err := syncOne(testActive(), "payments"); err != nil { + t.Fatalf("syncOne: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, "OLD.txt")); !os.IsNotExist(err) { + t.Error("old file should be gone after atomic replace") + } + if b, err := os.ReadFile(filepath.Join(dir, "NEW.txt")); err != nil || string(b) != "new" { + t.Errorf("new file missing after replace: %q err=%v", b, err) + } + // No leftover temp or backup dirs. + entries, _ := os.ReadDir(igHomeDir) + for _, e := range entries { + if strings.HasPrefix(e.Name(), ".sync-") { + t.Errorf("leftover temp dir under IG_HOME: %s", e.Name()) + } + } + projEntries, _ := os.ReadDir(filepath.Join(igHomeDir, "projects")) + for _, e := range projEntries { + if strings.Contains(e.Name(), ".bak") { + t.Errorf("leftover backup dir: %s", e.Name()) + } + } +} + +// --- sync: enforces the archive contract (metadata.json at the root) --- + +// A well-formed bundle carries the catalog directory's *contents*: +// metadata.json at the archive root plus member//graphify-out/graph.json. +// It must swap in cleanly and leave metadata.json readable at the tree root. +func TestIgSync_GoodBundleWithMetadataAtRootSwapsIn(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + tarball := makeTarGz(t, map[string]string{ + "metadata.json": `{"catalog":"capillary-cloud"}`, + "member/api/graphify-out/graph.json": `{"m":"api"}`, + "catalog/graphify-out/graph.json": `{"root":true}`, + }) + etag := sha256Etag(tarball) + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, _ string) ([]byte, string, bool, error) { + return tarball, etag, false, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + if _, err := syncOne(testActive(), "capillary-cloud"); err != nil { + t.Fatalf("good bundle should sync: %v", err) + } + dir := filepath.Join(igHomeDir, "projects", "capillary-cloud") + if b, err := os.ReadFile(filepath.Join(dir, "metadata.json")); err != nil || string(b) != `{"catalog":"capillary-cloud"}` { + t.Errorf("metadata.json must land at the catalog root: %q err=%v", b, err) + } + if b, err := os.ReadFile(filepath.Join(dir, "member", "api", "graphify-out", "graph.json")); err != nil || string(b) != `{"m":"api"}` { + t.Errorf("member graph missing after sync: %q err=%v", b, err) + } + // The double-nest must NOT be present. + if _, err := os.Stat(filepath.Join(dir, "capillary-cloud")); err == nil { + t.Error("catalog should not be double-nested under itself") + } +} + +// A malformed bundle must be rejected AFTER extraction but BEFORE the swap, +// so the pre-existing live tree is left untouched and readable. The two +// shapes we guard against: +// - a catalog-prefixed archive (/metadata.json, ...) — the exact +// double-nesting regression we hit in production; +// - an archive with no metadata.json anywhere. +func TestIgSync_RejectsMalformedBundleAndKeepsLiveTree(t *testing.T) { + cases := []struct { + name string + files map[string]string + wantInError string + }{ + { + name: "catalog-prefixed archive", + files: map[string]string{ + "capillary-cloud/metadata.json": `{"catalog":"capillary-cloud"}`, + "capillary-cloud/member/api/graphify-out/graph.json": `{"m":"api"}`, + }, + wantInError: "catalog-prefixed", + }, + { + name: "no metadata.json anywhere", + files: map[string]string{ + "member/api/graphify-out/graph.json": `{"m":"api"}`, + }, + wantInError: "no metadata.json", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + // Seed a healthy, readable live tree that MUST survive. + dir := filepath.Join(igHomeDir, "projects", "capillary-cloud") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "metadata.json"), []byte(`LIVE-KEEP-ME`), 0o644); err != nil { + t.Fatal(err) + } + if err := writeSyncState(dir, syncState{Digest: "sha256:old"}); err != nil { + t.Fatal(err) + } + + tarball := makeTarGz(t, tc.files) + etag := sha256Etag(tarball) + orig := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, _ string) ([]byte, string, bool, error) { + return tarball, etag, false, nil + } + defer func() { igcatalog.DownloadBundle = orig }() + + _, err := syncOne(testActive(), "capillary-cloud") + if err == nil { + t.Fatal("malformed bundle was silently accepted; want a non-nil contract error") + } + if !errors.Is(err, errBundleContract) { + t.Errorf("error = %v; want errBundleContract", err) + } + if !strings.Contains(err.Error(), tc.wantInError) { + t.Errorf("error %q must mention %q", err.Error(), tc.wantInError) + } + + // The live tree is untouched and still readable. + if b, err := os.ReadFile(filepath.Join(dir, "metadata.json")); err != nil || string(b) != "LIVE-KEEP-ME" { + t.Errorf("live tree must survive a rejected sync; metadata.json = %q err=%v", b, err) + } + if st, _ := readSyncState(dir); st.Digest != "sha256:old" { + t.Errorf("rejected sync must not rewrite .sync.json; got %+v", st) + } + }) + } +} + +// --- status: compares without downloading the bundle ------------------- + +func TestIgStatus_ComparesWithoutFetchingBundle(t *testing.T) { + igHomeDir := t.TempDir() + t.Setenv("IG_HOME", igHomeDir) + resetIgFlags() + defer resetIgFlags() + + dir := filepath.Join(igHomeDir, "projects", "payments") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := writeSyncState(dir, syncState{Digest: "sha256:local"}); err != nil { + t.Fatal(err) + } + + origGet := igcatalog.GetCatalog + igcatalog.GetCatalog = func(_, _, name string) (*igcatalog.Catalog, error) { + return &igcatalog.Catalog{Name: name, Version: "sha256:server-newer"}, nil + } + defer func() { igcatalog.GetCatalog = origGet }() + + origDL := igcatalog.DownloadBundle + igcatalog.DownloadBundle = func(_, _, _, _ string) ([]byte, string, bool, error) { + t.Fatal("status must NOT download the bundle") + return nil, "", false, nil + } + defer func() { igcatalog.DownloadBundle = origDL }() + + state, serverVer, localDigest, err := statusOne(testActive(), "payments") + if err != nil { + t.Fatalf("statusOne: %v", err) + } + if state != "stale" { + t.Errorf("state = %q; want stale", state) + } + if serverVer != "sha256:server-newer" || localDigest != "sha256:local" { + t.Errorf("serverVer=%q localDigest=%q", serverVer, localDigest) + } + + igcatalog.GetCatalog = func(_, _, name string) (*igcatalog.Catalog, error) { + return &igcatalog.Catalog{Name: name, Version: "sha256:local"}, nil + } + state, _, _, err = statusOne(testActive(), "payments") + if err != nil { + t.Fatal(err) + } + if state != "up to date" { + t.Errorf("state = %q; want up to date", state) + } +} + +// --- publish: gzips graph, reads git/sha from member-meta.json --------- + +func TestIgPublish_GzipsGraphAndReadsMeta(t *testing.T) { + seedIgProfile(t, "default") + resetIgFlags() + defer resetIgFlags() + + dir := t.TempDir() + graphDir := filepath.Join(dir, "member", "api", "graphify-out") + if err := os.MkdirAll(graphDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(graphDir, "graph.json"), []byte(`{"g":1}`), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "member", "api", "member-meta.json"), + []byte(`{"git":"https://github.com/acme/api.git","sha":"abc123"}`), 0o644); err != nil { + t.Fatal(err) + } + + igPublishCatalog = "payments" + igPublishMember = "api" + + var gotGz []byte + var gotGit, gotSha string + orig := igcatalog.PublishMember + igcatalog.PublishMember = func(_, _, cat, mem string, gz []byte, git, sha string) error { + if cat != "payments" || mem != "api" { + t.Errorf("cat=%q mem=%q", cat, mem) + } + gotGz, gotGit, gotSha = gz, git, sha + return nil + } + defer func() { igcatalog.PublishMember = orig }() + + var buf bytes.Buffer + igPublishCmd.SetOut(&buf) + igPublishCmd.SetErr(&buf) + if err := igPublishCmd.RunE(igPublishCmd, []string{dir}); err != nil { + t.Fatalf("RunE: %v", err) + } + zr, err := gzip.NewReader(bytes.NewReader(gotGz)) + if err != nil { + t.Fatalf("uploaded body not gzip: %v", err) + } + raw, _ := io.ReadAll(zr) + if string(raw) != `{"g":1}` { + t.Errorf("uploaded graph = %q; want the gzipped graph.json", raw) + } + if gotGit != "https://github.com/acme/api.git" || gotSha != "abc123" { + t.Errorf("git=%q sha=%q; want values from member-meta.json", gotGit, gotSha) + } +} + +// --- claims: one catalog name per line --------------------------------- + +func TestIgClaims_OnePerLine(t *testing.T) { + seedIgProfile(t, "default") + resetIgFlags() + defer resetIgFlags() + igClaimsGit = "https://github.com/acme/api.git" + + orig := igcatalog.Claims + igcatalog.Claims = func(_, _, git string) ([]string, error) { + if git != "https://github.com/acme/api.git" { + t.Errorf("git = %q", git) + } + return []string{"payments", "identity"}, nil + } + defer func() { igcatalog.Claims = orig }() + + var buf bytes.Buffer + igClaimsCmd.SetOut(&buf) + if err := igClaimsCmd.RunE(igClaimsCmd, nil); err != nil { + t.Fatalf("RunE: %v", err) + } + if buf.String() != "payments\nidentity\n" { + t.Errorf("claims output = %q; want one catalog per line", buf.String()) + } +} + +// --- manifest push: stamps git_sha + pushed_by ------------------------- + +func TestIgManifestPush_StampsGitSHA(t *testing.T) { + seedIgProfile(t, "default") + resetIgFlags() + defer resetIgFlags() + + file := filepath.Join(t.TempDir(), "manifest.yaml") + if err := os.WriteFile(file, []byte("catalog: payments\n"), 0o644); err != nil { + t.Fatal(err) + } + igManifestCatalog = "payments" + + origGit := gitHeadSHA + gitHeadSHA = func(dir string) (string, error) { return "cafe42", nil } + defer func() { gitHeadSHA = origGit }() + + var got igcatalog.Manifest + orig := igcatalog.ManifestPush + igcatalog.ManifestPush = func(_, _, cat string, m igcatalog.Manifest) error { + if cat != "payments" { + t.Errorf("cat = %q", cat) + } + got = m + return nil + } + defer func() { igcatalog.ManifestPush = orig }() + + var buf bytes.Buffer + igManifestPushCmd.SetOut(&buf) + if err := igManifestPushCmd.RunE(igManifestPushCmd, []string{file}); err != nil { + t.Fatalf("RunE: %v", err) + } + if got.Content != "catalog: payments\n" { + t.Errorf("content = %q", got.Content) + } + if got.GitSHA != "cafe42" { + t.Errorf("git_sha = %q; want cafe42 (stamped from the working tree)", got.GitSHA) + } + if got.PushedBy != "u@x.com" { + t.Errorf("pushed_by = %q; want the profile username", got.PushedBy) + } + if got.PushedAt == "" { + t.Error("pushed_at empty") + } +} + +// --- manifest pull: writes served bytes verbatim ----------------------- + +func TestIgManifestPull_WritesServedBytesVerbatim(t *testing.T) { + seedIgProfile(t, "default") + resetIgFlags() + defer resetIgFlags() + + outFile := filepath.Join(t.TempDir(), "pulled.yaml") + igManifestOut = outFile + + orig := igcatalog.ManifestPull + igcatalog.ManifestPull = func(_, _, cat string) (*igcatalog.Manifest, error) { + if cat != "payments" { + t.Errorf("cat = %q", cat) + } + return &igcatalog.Manifest{Content: "served: true\n"}, nil + } + defer func() { igcatalog.ManifestPull = orig }() + + var buf bytes.Buffer + igManifestPullCmd.SetOut(&buf) + if err := igManifestPullCmd.RunE(igManifestPullCmd, []string{"payments"}); err != nil { + t.Fatalf("RunE: %v", err) + } + b, err := os.ReadFile(outFile) + if err != nil { + t.Fatal(err) + } + if string(b) != "served: true\n" { + t.Errorf("pulled file = %q; want the served bytes verbatim", b) + } +} + +// --- the seven verbs are wired under `ig` ------------------------------ + +func TestIgCommandTree_HasSevenVerbs(t *testing.T) { + have := map[string]bool{} + for _, c := range igCmd.Commands() { + have[c.Name()] = true + } + for _, want := range []string{"list", "sync", "status", "publish", "claims", "manifest"} { + if !have[want] { + t.Errorf("praxis ig is missing subcommand %q", want) + } + } + man := map[string]bool{} + for _, c := range igManifestCmd.Commands() { + man[c.Name()] = true + } + if !man["push"] || !man["pull"] { + t.Errorf("praxis ig manifest is missing push/pull; have %v", man) + } +} diff --git a/internal/igcatalog/conformance_test.go b/internal/igcatalog/conformance_test.go new file mode 100644 index 0000000..f8f1d78 --- /dev/null +++ b/internal/igcatalog/conformance_test.go @@ -0,0 +1,233 @@ +package igcatalog + +// Schema-conformance tests: decode JSON examples derived from the LIVE Praxis +// OpenAPI (captured verbatim in testdata/ig-openapi.json) into the Go types +// this client actually decodes, and assert the round-trip holds. This is the +// guard that would have caught every ig client/server drift before a human +// ran the binary: +// +// - claims decoded a {git,catalogs} envelope as a bare []string, +// - Catalog.Members decoded member objects as []string, +// - Manifest dropped the required `catalog` field. +// +// The server is authoritative: these tests read its schema, never the other +// way round. Adding a new GET verb is cheap — capture a fresh openapi into +// testdata, add one row to conformanceCases, and the coverage guard below +// tells you if you forgot. + +import ( + _ "embed" + "encoding/json" + "strings" + "testing" +) + +//go:embed testdata/ig-openapi.json +var igOpenAPIJSON []byte + +// --- minimal OpenAPI shapes we assert against -------------------------- + +type openAPIDoc struct { + Paths map[string]map[string]openAPIOp `json:"paths"` + Components struct { + Schemas map[string]openAPISchema `json:"schemas"` + } `json:"components"` +} + +type openAPIOp struct { + Responses map[string]struct { + Content map[string]struct { + Schema openAPISchema `json:"schema"` + } `json:"content"` + } `json:"responses"` +} + +type openAPISchema struct { + Ref string `json:"$ref"` + Type string `json:"type"` + Required []string `json:"required"` + Properties map[string]json.RawMessage `json:"properties"` + Items *openAPISchema `json:"items"` +} + +func loadOpenAPI(t *testing.T) openAPIDoc { + t.Helper() + var doc openAPIDoc + if err := json.Unmarshal(igOpenAPIJSON, &doc); err != nil { + t.Fatalf("parse embedded ig-openapi.json: %v", err) + } + if len(doc.Components.Schemas) == 0 { + t.Fatalf("no component schemas in ig-openapi.json fixture") + } + return doc +} + +func refName(s openAPISchema) string { + if s.Ref != "" { + return strings.TrimPrefix(s.Ref, "#/components/schemas/") + } + if s.Items != nil && s.Items.Ref != "" { + return strings.TrimPrefix(s.Items.Ref, "#/components/schemas/") + } + return "" +} + +func contains(ss []string, want string) bool { + for _, s := range ss { + if s == want { + return true + } + } + return false +} + +// --- the conformance table --------------------------------------------- +// +// One row per response type this client decodes. `example` is a full JSON +// object (every property populated) drawn from the live shapes; the test +// verifies it honors the schema's required set, decodes cleanly, and that +// every required field survives the decode into the Go type. +var conformanceCases = []struct { + schema string // component schema name in the OpenAPI + target func() any // fresh pointer to the Go type the client decodes into + example string // full JSON example (every property populated) +}{ + { + schema: "IgCatalogSummary", + target: func() any { return new(Catalog) }, + example: `{"name":"capillary-cloud","version":"2026.07.10-104039","built_at":"2026-07-10T10:40:39Z","members":[{"name":"control-plane","kind":"code","git":"github.com/facets-cloud/control-plane","sha":"9a7c76c51a85da8dee3307bac95f85318961e8f9"}]}`, + }, + { + schema: "IgMemberMeta", + target: func() any { return new(Member) }, + example: `{"name":"control-plane","kind":"code","git":"github.com/facets-cloud/control-plane","sha":"9a7c76c51a85da8dee3307bac95f85318961e8f9"}`, + }, + { + schema: "IgClaimsResponse", + target: func() any { return new(claimsResponse) }, + example: `{"git":"github.com/facets-cloud/control-plane","catalogs":["capillary-cloud"]}`, + }, + { + schema: "IgManifest", + target: func() any { return new(Manifest) }, + example: `{"catalog":"capillary-cloud","content":"name: capillary-cloud\nrepos: []\n","pushed_by":"a@b.com","pushed_at":"2026-07-10T10:40:39Z","git_sha":"9a7c76c51a85da8dee3307bac95f85318961e8f9"}`, + }, +} + +// TestConformance_ResponseTypesMatchSchema is the core guard. For each response +// type the client decodes it proves three things against the LIVE schema: +// +// 1. a schema-shaped example decodes into the Go type without error, +// 2. every field the schema marks REQUIRED survives the decode (a Go type +// that drops a required field, or decodes it empty, fails here), and +// 3. null optional fields and unknown future fields do not break the decode. +func TestConformance_ResponseTypesMatchSchema(t *testing.T) { + doc := loadOpenAPI(t) + for _, tc := range conformanceCases { + t.Run(tc.schema, func(t *testing.T) { + sch, ok := doc.Components.Schemas[tc.schema] + if !ok { + t.Fatalf("schema %q is not in the OpenAPI fixture", tc.schema) + } + + // 0. The example must itself honor the schema's required set, so a + // lazy example can't paper over a dropped field. + var exMap map[string]json.RawMessage + if err := json.Unmarshal([]byte(tc.example), &exMap); err != nil { + t.Fatalf("example is not valid JSON: %v", err) + } + for _, req := range sch.Required { + if _, ok := exMap[req]; !ok { + t.Fatalf("example for %s omits required field %q; fix the fixture", tc.schema, req) + } + } + + // 1. The example decodes into the Go type without error. + full := tc.target() + if err := json.Unmarshal([]byte(tc.example), full); err != nil { + t.Fatalf("decode example into %T: %v", full, err) + } + + // 2. Round-trip: re-marshal and confirm every required field is + // still there and non-empty. A Go type missing the field + // (Manifest without `catalog`) drops it on re-marshal → caught. + round, err := json.Marshal(full) + if err != nil { + t.Fatalf("re-marshal %T: %v", full, err) + } + var got map[string]json.RawMessage + if err := json.Unmarshal(round, &got); err != nil { + t.Fatalf("re-parse round-trip of %T: %v", full, err) + } + for _, req := range sch.Required { + raw, present := got[req] + if !present { + t.Errorf("%s: required field %q is dropped by %T (add it to the Go type)", tc.schema, req, full) + continue + } + if s := strings.TrimSpace(string(raw)); s == "null" || s == `""` { + t.Errorf("%s: required field %q decoded empty (%s) in %T", tc.schema, req, s, full) + } + } + + // 3. Tolerance: every optional/nullable field set to null, plus an + // unknown future field, must still decode cleanly. + tol := make(map[string]json.RawMessage, len(exMap)+1) + for k, v := range exMap { + tol[k] = v + } + for name := range sch.Properties { + if !contains(sch.Required, name) { + tol[name] = json.RawMessage("null") + } + } + tol["__unknown_future_field__"] = json.RawMessage(`{"added":"later"}`) + tolJSON, err := json.Marshal(tol) + if err != nil { + t.Fatalf("build tolerance example: %v", err) + } + if err := json.Unmarshal(tolJSON, tc.target()); err != nil { + t.Errorf("%s: decode with null optionals + unknown field failed: %v\n%s", tc.schema, err, tolJSON) + } + }) + } +} + +// TestConformance_EveryGETResponseTypeIsCovered makes the table hard to forget: +// every GET under /ig/ whose 200 body is a component schema (directly or as an +// array element) MUST have a conformanceCases row. Binary/empty-schema bodies +// (bundle download, member blob download) legitimately have no ref and are +// skipped. Add a GET verb, refresh the fixture, and this test names exactly +// which type you still owe a row. +func TestConformance_EveryGETResponseTypeIsCovered(t *testing.T) { + doc := loadOpenAPI(t) + covered := make(map[string]bool, len(conformanceCases)) + for _, tc := range conformanceCases { + covered[tc.schema] = true + } + + for path, methods := range doc.Paths { + if !strings.Contains(path, "/ig/") { + continue + } + op, ok := methods["get"] + if !ok { + continue + } + resp, ok := op.Responses["200"] + if !ok { + continue + } + media, ok := resp.Content["application/json"] + if !ok { + continue + } + name := refName(media.Schema) + if name == "" { + continue // binary or empty-schema body (bundle, member download) + } + if !covered[name] { + t.Errorf("GET %s returns %s but no conformanceCases row covers it — add one", path, name) + } + } +} diff --git a/internal/igcatalog/igcatalog.go b/internal/igcatalog/igcatalog.go new file mode 100644 index 0000000..56a16ab --- /dev/null +++ b/internal/igcatalog/igcatalog.go @@ -0,0 +1,398 @@ +// Package igcatalog is the REST client for the ig catalog server on a +// Praxis deployment (mounted at /ai-api/ig, org-scoped). It is the ONLY +// network client of that server: `praxis ig ` calls here; the `ig` +// binary itself never learns servers exist — it reads the filesystem that +// `praxis ig sync` materializes. +// +// The package mirrors the layout of internal/duties and internal/memory: +// typed structs track the server's response models, exported function vars +// give tests a seam to swap, and every transport call sends +// Authorization: Bearer (the same bearer these clients already +// send — the server resolves it via auth_service.validate_user()). +// +// Backend routes (all under /ai-api/ig, org-scoped): +// +// GET /catalogs list the org's catalogs +// GET /catalogs/claims?git= names of catalogs claiming a repo +// GET /catalogs/{c} one catalog's summary (404 if absent) +// POST /catalogs/{c}/members/{m} publish one member (gzipped graph.json) +// GET /catalogs/{c}/bundle assembled catalog as a gzipped tarball +// POST /catalogs/{c}/manifest push the manifest text + stamps +// GET /catalogs/{c}/manifest pull the manifest text + stamps +package igcatalog + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + // apiPrefix is the deployment's ig-API mount (mirrors + // internal/memory's /ai-api/memories and duties' /ai-api). + apiPrefix = "/ai-api/ig" + + defaultTimeout = 30 * time.Second + + // bundleTimeout is longer than defaultTimeout: a catalog bundle is a + // gzipped tarball of the whole graph-of-graphs and can be large. + bundleTimeout = 120 * time.Second +) + +// Catalog is the wire shape of a catalog summary: {name, version, +// built_at, members[]}. Only the fields the CLI surfaces are retained — +// encoding/json ignores the rest, so older binaries keep working as the +// server adds fields. +// +// Version doubles as the bundle's ETag / the stored .sync.json digest: it +// is the single opaque token the server hands out to identify a built +// catalog. `praxis ig status` compares the local digest against this +// Version without downloading the bundle; `praxis ig sync` sends it as +// If-None-Match to get a cheap 304 when nothing changed. +type Catalog struct { + Name string `json:"name"` + Version string `json:"version"` + BuiltAt string `json:"built_at"` + Members []Member `json:"members"` +} + +// Member is the wire shape of one catalog member, mirroring the entries in +// ig's own metadata.json: a name, a kind ("code" or "infra"), and — for +// code members — the canonical git URL and the built commit sha. The infra +// member carries no repo, so Git/SHA are absent or JSON null on the wire; +// both decode to the empty string. omitempty keeps `praxis ig list --json` +// output tidy for members without a repo. +type Member struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty"` + Git string `json:"git,omitempty"` + SHA string `json:"sha,omitempty"` +} + +// Manifest is the wire shape of a served manifest (the server's IgManifest): +// the catalog it belongs to, the text, plus the stamps the server records on +// push (who pushed it, when, and the git sha of the working tree the file came +// from). All four of Catalog/Content/PushedBy/PushedAt are required on the +// wire; GitSHA is nullable. Catalog is retained so the response type captures +// every field the server declares required, even though the CLI currently only +// surfaces Content. +type Manifest struct { + Catalog string `json:"catalog,omitempty"` + Content string `json:"content"` + PushedBy string `json:"pushed_by,omitempty"` + PushedAt string `json:"pushed_at,omitempty"` + GitSHA string `json:"git_sha,omitempty"` +} + +// manifestPushRequest is the wire shape the server accepts on manifest push +// (its IgManifestPushRequest): ONLY the text and an optional git sha. The +// server stamps pushed_by (from the bearer's identity) and pushed_at (server +// time) itself, so the client must NOT send them — reusing the Manifest +// response type here would put fields on the wire the server never declared. +type manifestPushRequest struct { + Content string `json:"content"` + GitSHA string `json:"git_sha,omitempty"` +} + +// claimsResponse is the envelope the server returns from GET /catalogs/claims +// (its IgClaimsResponse): the echoed git URL plus the names of the catalogs +// claiming it. The server does NOT return a bare []string — decoding it as one +// is what made `praxis ig claims` die with "cannot unmarshal object into Go +// value of type []string". Only Git is required; Catalogs is absent (nil) when +// no catalog claims the repo. +type claimsResponse struct { + Git string `json:"git"` + Catalogs []string `json:"catalogs"` +} + +// --- HTTP seams — tests swap these to avoid the network. --------------- + +// ListCatalogs returns every catalog in the org. +var ListCatalogs = func(baseURL, token string) ([]Catalog, error) { + return doJSON[[]Catalog](baseURL, token, http.MethodGet, apiPrefix+"/catalogs", nil) +} + +// GetCatalog returns one catalog's summary. The server 404s when the +// catalog is absent; that surfaces as an `HTTP 404 …` error. +var GetCatalog = func(baseURL, token, name string) (*Catalog, error) { + if name == "" { + return nil, fmt.Errorf("catalog name is required") + } + c, err := doJSON[Catalog](baseURL, token, http.MethodGet, + apiPrefix+"/catalogs/"+url.PathEscape(name), nil) + if err != nil { + return nil, err + } + return &c, nil +} + +// Claims returns the names of catalogs that have a member whose canonical +// git URL matches git. Repo CI loops over these to know which catalogs to +// refresh after a push. +var Claims = func(baseURL, token, git string) ([]string, error) { + if git == "" { + return nil, fmt.Errorf("git url is required") + } + q := url.Values{} + q.Set("git", git) + env, err := doJSON[claimsResponse](baseURL, token, http.MethodGet, + apiPrefix+"/catalogs/claims?"+q.Encode(), nil) + if err != nil { + return nil, err + } + return env.Catalogs, nil +} + +// PublishMember uploads one member's gzipped graph.json to a catalog, +// stamping the member's canonical git URL and commit sha. Server-side it +// is idempotent — republishing the same (git, sha) is accepted. +// +// The server handler (publish_member) expects multipart/form-data: a file +// part named "graph" carrying the gzipped graph.json bytes, plus optional +// "git"/"sha" form fields. On the server those are Optional[...] = Form(None), +// so they are written only when non-empty; git/sha are NOT query parameters. +var PublishMember = func(baseURL, token, catalog, member string, gzGraph []byte, git, sha string) error { + if catalog == "" || member == "" { + return fmt.Errorf("catalog and member are required") + } + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile("graph", "graph.json.gz") + if err != nil { + return err + } + if _, err := part.Write(gzGraph); err != nil { + return err + } + if git != "" { + if err := writer.WriteField("git", git); err != nil { + return err + } + } + if sha != "" { + if err := writer.WriteField("sha", sha); err != nil { + return err + } + } + if err := writer.Close(); err != nil { + return err + } + + // FormDataContentType() carries the boundary — never hand-roll it. + path := apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/members/" + url.PathEscape(member) + return sendBytes(baseURL, token, http.MethodPost, path, writer.FormDataContentType(), body.Bytes()) +} + +// DownloadBundle fetches the assembled catalog as a gzipped tarball. +// ifNoneMatch is the caller's last-synced digest; when it matches the +// server's current ETag the server returns 304 and this reports +// notModified=true with an empty body (a cheap no-op re-sync). On 200 it +// returns the tarball bytes and the ETag (the new digest). +var DownloadBundle = func(baseURL, token, catalog, ifNoneMatch string) (body []byte, etag string, notModified bool, err error) { + if baseURL == "" { + return nil, "", false, fmt.Errorf("baseURL is required") + } + if token == "" { + return nil, "", false, fmt.Errorf("token is required") + } + if catalog == "" { + return nil, "", false, fmt.Errorf("catalog is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), bundleTimeout) + defer cancel() + + full := strings.TrimRight(baseURL, "/") + apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/bundle" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil) + if err != nil { + return nil, "", false, err + } + req.Header.Set("Authorization", "Bearer "+token) + if ifNoneMatch != "" { + req.Header.Set("If-None-Match", quoteETag(ifNoneMatch)) + } + + client := &http.Client{Timeout: bundleTimeout} + resp, err := client.Do(req) + if err != nil { + return nil, "", false, err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusNotModified { + return nil, unquoteETag(resp.Header.Get("ETag")), true, nil + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", false, fmt.Errorf("read body: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, "", false, fmt.Errorf("HTTP %d from %s: %s", resp.StatusCode, full, truncate(string(raw), 200)) + } + return raw, unquoteETag(resp.Header.Get("ETag")), false, nil +} + +// unquoteETag strips the surrounding double quotes an HTTP ETag header is +// required to carry (RFC 9110 §8.8.3: entity-tag = [ weak ] opaque-tag, +// opaque-tag = DQUOTE *etagc DQUOTE), plus a leading weak-validator "W/" +// prefix if present, so callers only ever see the bare tag. Those quotes +// are HTTP syntax, not catalog data: `praxis ig sync` used to store the +// header verbatim, leaking `"` into .sync.json's digest/from fields and +// therefore into `ig`'s human-facing provenance footer. A value that +// isn't quoted (e.g. already-bare test fixtures) passes through +// unchanged. +func unquoteETag(raw string) string { + v := strings.TrimSpace(raw) + v = strings.TrimPrefix(v, "W/") + if len(v) >= 2 && strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) { + v = v[1 : len(v)-1] + } + return v +} + +// quoteETag is unquoteETag's inverse for the send side: If-None-Match must +// carry a properly quoted validator per RFC 9110, but callers now pass the +// bare digest they read out of .sync.json (post unquoteETag). A value +// that's already quoted (e.g. a digest cached before this fix shipped) is +// left alone rather than double-quoted. +func quoteETag(v string) string { + if v == "" || (len(v) >= 2 && strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) { + return v + } + return `"` + v + `"` +} + +// ManifestPush uploads the manifest text and its git sha. The server stamps +// pushed_by/pushed_at itself, so only content and git_sha go on the wire (the +// server's IgManifestPushRequest) — m.PushedBy/m.PushedAt are ignored here and +// exist only for the cmd layer's local echo. +var ManifestPush = func(baseURL, token, catalog string, m Manifest) error { + if catalog == "" { + return fmt.Errorf("catalog is required") + } + body, err := json.Marshal(manifestPushRequest{Content: m.Content, GitSHA: m.GitSHA}) + if err != nil { + return err + } + path := apiPrefix + "/catalogs/" + url.PathEscape(catalog) + "/manifest" + return sendBytes(baseURL, token, http.MethodPost, path, "application/json", body) +} + +// ManifestPull fetches the served manifest (text + stamps) so a builder +// can diff it against their local copy. +var ManifestPull = func(baseURL, token, catalog string) (*Manifest, error) { + if catalog == "" { + return nil, fmt.Errorf("catalog is required") + } + m, err := doJSON[Manifest](baseURL, token, http.MethodGet, + apiPrefix+"/catalogs/"+url.PathEscape(catalog)+"/manifest", nil) + if err != nil { + return nil, err + } + return &m, nil +} + +// --- transport --------------------------------------------------------- + +// doJSON is the shared JSON transport. Returns a typed payload or an error +// shaped as `HTTP from : ` so the cmd layer can +// branch on status (401/403 → auth) without re-parsing the URL. Copied +// deliberately from internal/duties to keep the clients' error contracts +// identical. +func doJSON[T any](baseURL, token, method, path string, body io.Reader) (T, error) { + var zero T + if baseURL == "" { + return zero, fmt.Errorf("baseURL is required") + } + if token == "" { + return zero, fmt.Errorf("token is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + full := strings.TrimRight(baseURL, "/") + path + req, err := http.NewRequestWithContext(ctx, method, full, body) + if err != nil { + return zero, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json") + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + client := &http.Client{Timeout: defaultTimeout} + resp, err := client.Do(req) + if err != nil { + return zero, err + } + defer func() { _ = resp.Body.Close() }() + raw, err := io.ReadAll(resp.Body) + if err != nil { + return zero, fmt.Errorf("read body: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return zero, fmt.Errorf("HTTP %d from %s: %s", resp.StatusCode, full, truncate(string(raw), 200)) + } + + var out T + if err := json.Unmarshal(raw, &out); err != nil { + return zero, fmt.Errorf("parse response: %w", err) + } + return out, nil +} + +// sendBytes POSTs (or PUTs) a raw body with the bearer header and a caller- +// chosen Content-Type, tolerating any 2xx with any (or empty) response +// body. Used for the two non-JSON-returning uploads: the gzipped member +// graph and the manifest push. The error contract matches doJSON so the +// cmd layer's reportHTTPErr dispatch works the same. +func sendBytes(baseURL, token, method, path, contentType string, body []byte) error { + if baseURL == "" { + return fmt.Errorf("baseURL is required") + } + if token == "" { + return fmt.Errorf("token is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + full := strings.TrimRight(baseURL, "/") + path + req, err := http.NewRequestWithContext(ctx, method, full, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+token) + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + client := &http.Client{Timeout: defaultTimeout} + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("HTTP %d from %s: %s", resp.StatusCode, full, truncate(string(raw), 200)) + } + return nil +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] + "…" +} diff --git a/internal/igcatalog/igcatalog_test.go b/internal/igcatalog/igcatalog_test.go new file mode 100644 index 0000000..4ee55f7 --- /dev/null +++ b/internal/igcatalog/igcatalog_test.go @@ -0,0 +1,587 @@ +package igcatalog + +import ( + "bytes" + "compress/gzip" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func assertReq(t *testing.T, r *http.Request, wantMethod, wantPath, wantBearer string) { + t.Helper() + if r.Method != wantMethod { + t.Errorf("method = %s; want %s", r.Method, wantMethod) + } + if r.URL.Path != wantPath { + t.Errorf("path = %s; want %s", r.URL.Path, wantPath) + } + if got := r.Header.Get("Authorization"); got != "Bearer "+wantBearer { + t.Errorf("auth header = %q; want %q", got, "Bearer "+wantBearer) + } +} + +// --- ListCatalogs ------------------------------------------------------ + +func TestListCatalogs_HappyPath(t *testing.T) { + const body = `[ + {"name":"payments","version":"sha256:aaa","built_at":"2026-07-09T10:00:00Z","members":[{"name":"api","kind":"code","git":"github.com/acme/api","sha":"a1"},{"name":"web","kind":"code","git":"github.com/acme/web","sha":"b2"}]}, + {"name":"identity","version":"sha256:bbb","built_at":"2026-07-08T09:00:00Z","members":[{"name":"idp","kind":"code","git":"github.com/acme/idp","sha":"c3"}]} + ]` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs", "tok") + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + got, err := ListCatalogs(srv.URL, "tok") + if err != nil { + t.Fatalf("ListCatalogs: %v", err) + } + if len(got) != 2 || got[0].Name != "payments" || len(got[0].Members) != 2 { + t.Fatalf("got = %+v", got) + } + if got[0].Version != "sha256:aaa" { + t.Errorf("version = %q", got[0].Version) + } +} + +// --- ListCatalogs: member objects (server contract) -------------------- +// +// The live server returns member OBJECTS ({name,kind,git,sha}), mirroring +// ig's own metadata.json — not a list of bare member-name strings. This is +// the exact payload `praxis ig list` choked on before Catalog.Members +// became []Member. + +func assertMember(t *testing.T, m Member, name, kind, git, sha string) { + t.Helper() + if m.Name != name || m.Kind != kind || m.Git != git || m.SHA != sha { + t.Errorf("member = %+v; want {Name:%s Kind:%s Git:%s SHA:%s}", m, name, kind, git, sha) + } +} + +func TestListCatalogs_DecodesMemberObjects(t *testing.T) { + const body = `[ + { + "name": "capillary-cloud", + "version": "2026.07.10-104039", + "built_at": "2026-07-10T10:40:39Z", + "members": [ + {"name": "control-plane", "kind": "code", "git": "github.com/facets-cloud/control-plane", "sha": "9a7c76c51a85da8dee3307bac95f85318961e8f9"}, + {"name": "control-plane-ui-react", "kind": "code", "git": "github.com/facets-cloud/control-plane-ui-react", "sha": "6fa97644570cc1b38968a350e8e63c723cc5b33b"}, + {"name": "raptor", "kind": "code", "git": "github.com/facets-cloud/raptor", "sha": "5032459d14e117d6b45033fce49c862ce588c842"}, + {"name": "agent-factory", "kind": "code", "git": "github.com/facets-cloud/agent-factory", "sha": "ae22e27b9d3f4a5c6e7f8091a2b3c4d5e6f70819"} + ] + } +]` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs", "tok") + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + got, err := ListCatalogs(srv.URL, "tok") + if err != nil { + t.Fatalf("ListCatalogs: %v", err) + } + if len(got) != 1 { + t.Fatalf("got %d catalogs; want 1: %+v", len(got), got) + } + c := got[0] + if c.Name != "capillary-cloud" || c.Version != "2026.07.10-104039" { + t.Errorf("catalog head = %+v", c) + } + if len(c.Members) != 4 { + t.Fatalf("got %d members; want 4: %+v", len(c.Members), c.Members) + } + assertMember(t, c.Members[0], "control-plane", "code", + "github.com/facets-cloud/control-plane", "9a7c76c51a85da8dee3307bac95f85318961e8f9") + assertMember(t, c.Members[2], "raptor", "code", + "github.com/facets-cloud/raptor", "5032459d14e117d6b45033fce49c862ce588c842") + assertMember(t, c.Members[3], "agent-factory", "code", + "github.com/facets-cloud/agent-factory", "ae22e27b9d3f4a5c6e7f8091a2b3c4d5e6f70819") +} + +// An infra member carries no repo: git/sha are absent (or explicit null). +// It must decode without error, leaving Git/SHA empty. +func TestListCatalogs_InfraMemberHasNoRepo(t *testing.T) { + const body = `[ + {"name":"cap","version":"v1","built_at":"2026-07-10T10:40:39Z","members":[ + {"name":"platform","kind":"code","git":"github.com/facets-cloud/platform","sha":"deadbeef"}, + {"name":"infra","kind":"infra"}, + {"name":"infra-null","kind":"infra","git":null,"sha":null} + ]} +]` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs", "tok") + _, _ = w.Write([]byte(body)) + })) + defer srv.Close() + + got, err := ListCatalogs(srv.URL, "tok") + if err != nil { + t.Fatalf("ListCatalogs: %v", err) + } + if len(got) != 1 || len(got[0].Members) != 3 { + t.Fatalf("got = %+v", got) + } + assertMember(t, got[0].Members[1], "infra", "infra", "", "") + // A member whose git/sha are explicit JSON null must also be tolerated. + assertMember(t, got[0].Members[2], "infra-null", "infra", "", "") +} + +// --- GetCatalog -------------------------------------------------------- + +func TestGetCatalog_404SurfacesError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/ghost", "tok") + w.WriteHeader(404) + _, _ = w.Write([]byte("no such catalog")) + })) + defer srv.Close() + + _, err := GetCatalog(srv.URL, "tok", "ghost") + if err == nil || !strings.Contains(err.Error(), "HTTP 404") { + t.Fatalf("err = %v; want HTTP 404", err) + } +} + +// --- Claims ------------------------------------------------------------ +// +// The live server returns an ENVELOPE — {"git":,"catalogs":[...]} — not +// a bare array of catalog names. Decoding it as []string is exactly the bug +// that made `praxis ig claims` die with +// +// network error: parse response: json: cannot unmarshal object into Go value of type []string +// +// Claims must peel the envelope and hand callers the catalog names so repo CI +// can loop over them. + +func TestClaims_EncodesGitAndReturnsNames(t *testing.T) { + const git = "https://github.com/acme/api.git" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/claims", "tok") + if r.URL.Query().Get("git") != git { + t.Errorf("git = %q; want %q", r.URL.Query().Get("git"), git) + } + _, _ = w.Write([]byte(`{"git":"` + git + `","catalogs":["payments","identity"]}`)) + })) + defer srv.Close() + + got, err := Claims(srv.URL, "tok", git) + if err != nil { + t.Fatalf("Claims: %v", err) + } + if len(got) != 2 || got[0] != "payments" || got[1] != "identity" { + t.Errorf("got = %+v", got) + } +} + +// The exact live payload for control-plane (from live-shapes.txt): a single +// claiming catalog. This is the byte-for-byte shape the broken client choked +// on in production. +func TestClaims_DecodesLiveEnvelope(t *testing.T) { + const git = "github.com/facets-cloud/control-plane" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/claims", "tok") + _, _ = w.Write([]byte(`{"git":"github.com/facets-cloud/control-plane","catalogs":["capillary-cloud"]}`)) + })) + defer srv.Close() + + got, err := Claims(srv.URL, "tok", git) + if err != nil { + t.Fatalf("Claims: %v", err) + } + if len(got) != 1 || got[0] != "capillary-cloud" { + t.Errorf("got = %+v; want [capillary-cloud]", got) + } +} + +// A repo no catalog claims: the server still returns the envelope, with +// catalogs absent or empty. Claims must yield an empty list, not an error. +func TestClaims_UnclaimedRepoIsEmptyNotError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/claims", "tok") + _, _ = w.Write([]byte(`{"git":"github.com/acme/orphan"}`)) + })) + defer srv.Close() + + got, err := Claims(srv.URL, "tok", "github.com/acme/orphan") + if err != nil { + t.Fatalf("Claims: %v", err) + } + if len(got) != 0 { + t.Errorf("got = %+v; want empty", got) + } +} + +// --- PublishMember (multipart/form-data upload) ------------------------ +// +// Contract (server handler publish_member): multipart/form-data with a file +// part named "graph" carrying the gzipped graph.json bytes, plus optional +// "git"/"sha" form fields (Optional[...] = Form(None)). git/sha are NOT +// query parameters, and are omitted entirely when empty. + +func gzipOf(t *testing.T, s string) []byte { + t.Helper() + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + if _, err := zw.Write([]byte(s)); err != nil { + t.Fatalf("gzip write: %v", err) + } + if err := zw.Close(); err != nil { + t.Fatalf("gzip close: %v", err) + } + return buf.Bytes() +} + +func TestPublishMember_UploadsMultipartWithGitAndSha(t *testing.T) { + const graph = `{"nodes":[{"id":"n1"}]}` + gz := gzipOf(t, graph) + + hit := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hit++ + assertReq(t, r, http.MethodPost, "/ai-api/ig/catalogs/payments/members/api", "tok") + + if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "multipart/form-data") { + t.Errorf("content-type = %q; want multipart/form-data", ct) + } + // git/sha must travel as form fields, never as query params. + if q := r.URL.Query().Get("git"); q != "" { + t.Errorf("git leaked into query string: %q", q) + } + if q := r.URL.Query().Get("sha"); q != "" { + t.Errorf("sha leaked into query string: %q", q) + } + + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Fatalf("body is not multipart/form-data: %v", err) + } + if got := r.FormValue("git"); got != "https://github.com/acme/api.git" { + t.Errorf("git form value = %q", got) + } + if got := r.FormValue("sha"); got != "abc123" { + t.Errorf("sha form value = %q", got) + } + + f, hdr, err := r.FormFile("graph") + if err != nil { + t.Fatalf("graph file part missing: %v", err) + } + defer func() { _ = f.Close() }() + if hdr.Filename == "" { + t.Error("graph part has no filename") + } + got, _ := io.ReadAll(f) + if !bytes.Equal(got, gz) { + t.Errorf("graph part bytes = %q; want the gzipped graph", got) + } + // The uploaded bytes are the gzip we handed in, decompressible. + zr, err := gzip.NewReader(bytes.NewReader(got)) + if err != nil { + t.Fatalf("graph part is not gzip: %v", err) + } + raw, _ := io.ReadAll(zr) + if string(raw) != graph { + t.Errorf("decompressed graph = %q; want %q", raw, graph) + } + w.WriteHeader(200) + })) + defer srv.Close() + + // First publish, then a repeat — the server accepts both (idempotent). + for i := 0; i < 2; i++ { + if err := PublishMember(srv.URL, "tok", "payments", "api", gz, "https://github.com/acme/api.git", "abc123"); err != nil { + t.Fatalf("PublishMember #%d: %v", i, err) + } + } + if hit != 2 { + t.Errorf("handler hit %d times; want 2", hit) + } +} + +func TestPublishMember_OmitsEmptyGitAndSha(t *testing.T) { + const graph = `{"nodes":[]}` + gz := gzipOf(t, graph) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodPost, "/ai-api/ig/catalogs/payments/members/api", "tok") + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Fatalf("body is not multipart/form-data: %v", err) + } + // Empty git/sha are absent — not sent as empty fields. + if vals, ok := r.MultipartForm.Value["git"]; ok { + t.Errorf("git should be absent when empty, got %v", vals) + } + if vals, ok := r.MultipartForm.Value["sha"]; ok { + t.Errorf("sha should be absent when empty, got %v", vals) + } + // The graph part is still present and intact. + f, _, err := r.FormFile("graph") + if err != nil { + t.Fatalf("graph file part missing: %v", err) + } + defer func() { _ = f.Close() }() + got, _ := io.ReadAll(f) + if !bytes.Equal(got, gz) { + t.Errorf("graph part bytes mismatch") + } + w.WriteHeader(200) + })) + defer srv.Close() + + if err := PublishMember(srv.URL, "tok", "payments", "api", gz, "", ""); err != nil { + t.Fatalf("PublishMember: %v", err) + } +} + +// --- DownloadBundle (200 with ETag, and 304 no-op) --------------------- + +func TestDownloadBundle_ReturnsBytesAndETag(t *testing.T) { + const tarball = "\x1f\x8bfake-gzipped-tarball" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/payments/bundle", "tok") + w.Header().Set("ETag", "sha256:deadbeef") + _, _ = w.Write([]byte(tarball)) + })) + defer srv.Close() + + body, etag, notModified, err := DownloadBundle(srv.URL, "tok", "payments", "") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if notModified { + t.Error("notModified should be false on 200") + } + if string(body) != tarball { + t.Errorf("body = %q", body) + } + if etag != "sha256:deadbeef" { + t.Errorf("etag = %q", etag) + } +} + +func TestDownloadBundle_SendsIfNoneMatchAnd304IsNoOp(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ifNoneMatch is passed in bare (as callers now store it, post + // unquoting); the wire header must be the properly quoted form. + if inm := r.Header.Get("If-None-Match"); inm != `"sha256:current"` { + t.Errorf("If-None-Match = %q; want the quoted validator %q", inm, `"sha256:current"`) + } + w.Header().Set("ETag", "sha256:current") + w.WriteHeader(http.StatusNotModified) + })) + defer srv.Close() + + body, etag, notModified, err := DownloadBundle(srv.URL, "tok", "payments", "sha256:current") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if !notModified { + t.Error("304 must report notModified=true") + } + if len(body) != 0 { + t.Errorf("304 must have empty body, got %q", body) + } + if etag != "sha256:current" { + t.Errorf("etag = %q", etag) + } +} + +// --- DownloadBundle: ETag is quoted HTTP syntax, not data (RFC 9110) --- +// +// RFC 9110 §8.8.3: entity-tag = [ weak ] opaque-tag, opaque-tag = DQUOTE +// *etagc DQUOTE. A real server's `ETag` header therefore always carries +// literal surrounding double quotes, and MAY carry a leading `W/` for a +// weak validator. Those are HTTP syntax, not catalog data: DownloadBundle +// must strip them so callers only ever see the bare tag. + +func TestDownloadBundle_StripsQuotedETag(t *testing.T) { + const tarball = "\x1f\x8bfake-gzipped-tarball" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", `"v1.2.3"`) + _, _ = w.Write([]byte(tarball)) + })) + defer srv.Close() + + _, etag, notModified, err := DownloadBundle(srv.URL, "tok", "payments", "") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if notModified { + t.Error("notModified should be false on 200") + } + if etag != "v1.2.3" { + t.Errorf("etag = %q; want the bare tag v1.2.3 with surrounding quotes stripped", etag) + } +} + +func TestDownloadBundle_StripsWeakValidatorPrefix(t *testing.T) { + const tarball = "\x1f\x8bfake-gzipped-tarball" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", `W/"v1.2.3"`) + _, _ = w.Write([]byte(tarball)) + })) + defer srv.Close() + + _, etag, _, err := DownloadBundle(srv.URL, "tok", "payments", "") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if etag != "v1.2.3" { + t.Errorf("etag = %q; want the bare tag v1.2.3 with W/ and quotes stripped", etag) + } +} + +func TestDownloadBundle_Strips304QuotedETag(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", `"v1.2.3"`) + w.WriteHeader(http.StatusNotModified) + })) + defer srv.Close() + + _, etag, notModified, err := DownloadBundle(srv.URL, "tok", "payments", "v1.2.3") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if !notModified { + t.Error("304 must report notModified=true") + } + if etag != "v1.2.3" { + t.Errorf("etag = %q; want the bare tag v1.2.3 on a 304 too", etag) + } +} + +// TestDownloadBundle_SendsProperlyQuotedIfNoneMatch is the send-side half +// of the fix: callers now pass a BARE digest (post-unquoting) as +// ifNoneMatch, so DownloadBundle must re-quote it before putting it on the +// wire — a bare unquoted token is not a valid If-None-Match value per RFC +// 9110. Before the fix this "worked" only by accident: the stored digest +// still carried its original quotes, so the bare-pass-through happened to +// produce valid syntax. Once the digest is stored bare, skipping this +// re-quote would break the 304 path against a real (spec-compliant) server. +func TestDownloadBundle_SendsProperlyQuotedIfNoneMatch(t *testing.T) { + var gotINM string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotINM = r.Header.Get("If-None-Match") + w.Header().Set("ETag", `"v1.2.3"`) + w.WriteHeader(http.StatusNotModified) + })) + defer srv.Close() + + _, _, notModified, err := DownloadBundle(srv.URL, "tok", "payments", "v1.2.3") + if err != nil { + t.Fatalf("DownloadBundle: %v", err) + } + if !notModified { + t.Error("304 must report notModified=true") + } + if gotINM != `"v1.2.3"` { + t.Errorf("If-None-Match sent = %q; want the quoted validator %q (a bare token is invalid HTTP syntax)", gotINM, `"v1.2.3"`) + } +} + +// --- Manifest push / pull ---------------------------------------------- + +// The server's IgManifestPushRequest declares ONLY content + git_sha. The +// server stamps pushed_by (from the bearer) and pushed_at (server time) +// itself, so the client must not put them on the wire even though the cmd +// layer hands ManifestPush a fully-populated Manifest for its local echo. +func TestManifestPush_SendsOnlyContentAndGitSHA(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodPost, "/ai-api/ig/catalogs/payments/manifest", "tok") + if ct := r.Header.Get("Content-Type"); ct != "application/json" { + t.Errorf("content-type = %q; want application/json", ct) + } + raw, _ := io.ReadAll(r.Body) + s := string(raw) + for _, want := range []string{`"content":"manifest-body"`, `"git_sha":"cafe42"`} { + if !strings.Contains(s, want) { + t.Errorf("push body missing %q\ngot: %s", want, s) + } + } + // Server-stamped fields must NOT be sent: they are not in the + // IgManifestPushRequest schema. + for _, forbidden := range []string{"pushed_by", "pushed_at", "u@x.com"} { + if strings.Contains(s, forbidden) { + t.Errorf("push body leaked server-stamped field %q\ngot: %s", forbidden, s) + } + } + w.WriteHeader(200) + })) + defer srv.Close() + + err := ManifestPush(srv.URL, "tok", "payments", Manifest{ + Content: "manifest-body", PushedBy: "u@x.com", PushedAt: "2026-07-09T10:00:00Z", GitSHA: "cafe42", + }) + if err != nil { + t.Fatalf("ManifestPush: %v", err) + } +} + +// git_sha is nullable/optional: when the manifest came from a non-git dir the +// client omits it entirely rather than sending an empty string. +func TestManifestPush_OmitsEmptyGitSHA(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + raw, _ := io.ReadAll(r.Body) + if s := string(raw); strings.Contains(s, "git_sha") { + t.Errorf("empty git_sha should be omitted; got: %s", s) + } + w.WriteHeader(200) + })) + defer srv.Close() + + if err := ManifestPush(srv.URL, "tok", "payments", Manifest{Content: "body-only"}); err != nil { + t.Fatalf("ManifestPush: %v", err) + } +} + +func TestManifestPull_ReturnsServedManifest(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertReq(t, r, http.MethodGet, "/ai-api/ig/catalogs/payments/manifest", "tok") + _, _ = w.Write([]byte(`{"catalog":"payments","content":"served-manifest","pushed_by":"a@b","pushed_at":"2026-07-10T10:00:00Z","git_sha":"c0ffee"}`)) + })) + defer srv.Close() + + m, err := ManifestPull(srv.URL, "tok", "payments") + if err != nil { + t.Fatalf("ManifestPull: %v", err) + } + if m.Catalog != "payments" || m.Content != "served-manifest" || m.GitSHA != "c0ffee" { + t.Errorf("manifest = %+v", m) + } +} + +// git_sha is nullable on the wire: an explicit null must decode to the empty +// string, not error. +func TestManifestPull_ToleratesNullGitSHA(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"catalog":"payments","content":"m","pushed_by":"a@b","pushed_at":"2026-07-10T10:00:00Z","git_sha":null}`)) + })) + defer srv.Close() + + m, err := ManifestPull(srv.URL, "tok", "payments") + if err != nil { + t.Fatalf("ManifestPull: %v", err) + } + if m.GitSHA != "" { + t.Errorf("git_sha = %q; want empty", m.GitSHA) + } +} + +// --- transport edge: token/baseURL required ---------------------------- + +func TestRequiresBaseURLAndToken(t *testing.T) { + if _, err := ListCatalogs("", "tok"); err == nil { + t.Error("want error for empty baseURL") + } + if _, err := ListCatalogs("http://x.test", ""); err == nil { + t.Error("want error for empty token") + } + if _, _, _, err := DownloadBundle("", "tok", "c", ""); err == nil { + t.Error("want error for empty baseURL on DownloadBundle") + } +} diff --git a/internal/igcatalog/testdata/ig-openapi.json b/internal/igcatalog/testdata/ig-openapi.json new file mode 100644 index 0000000..77f5329 --- /dev/null +++ b/internal/igcatalog/testdata/ig-openapi.json @@ -0,0 +1,19681 @@ +{ + "paths": { + "/ai-api/ig/catalogs": { + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "List Catalogs", + "description": "List the org's catalogs (name, version, built_at, members).\n\nBacks ``praxis ig list``.", + "operationId": "list_catalogs_ai_api_ig_catalogs_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/IgCatalogSummary" + }, + "type": "array", + "title": "Response List Catalogs Ai Api Ig Catalogs Get" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/claims": { + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "Catalogs Claiming", + "description": "Return the names of catalogs having a member with this canonical git URL.\n\nBacks ``praxis ig claims --git `` \u2014 the reverse lookup repo CI runs to\ndiscover which catalogs claim it. CI authenticates via an API-key Bearer\ntoken (``validate_user_cookie`` handles that). This literal path is declared\nbefore ``/catalogs/{catalog}`` so it is not captured as a catalog name.", + "operationId": "catalogs_claiming_ai_api_ig_catalogs_claims_get", + "parameters": [ + { + "name": "git", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "Canonical git URL to reverse-look-up", + "title": "Git" + }, + "description": "Canonical git URL to reverse-look-up" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgClaimsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/{catalog}": { + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "Get Catalog", + "description": "Return one catalog's summary; 404 if absent.", + "operationId": "get_catalog_ai_api_ig_catalogs__catalog__get", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgCatalogSummary" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/{catalog}/members/{member}": { + "post": { + "tags": [ + "ig-catalog" + ], + "summary": "Publish Member", + "description": "Publish one member \u2014 its gzipped ``graph.json`` bytes plus ``git``/``sha``.\n\nIdempotent by ``(org, catalog, member, sha)``: a repeat publish returns 200\nwith the same ``blob_id`` and creates no duplicate. Backs ``praxis ig\npublish``, which repo CI calls with an API-key Bearer token.\n\nAfter a successful publish we enqueue a background assemble for the catalog\n(house ``BackgroundTasks`` idiom) so a published member leads to a fresh\ncatalog instead of a silently stale one. A concurrent assemble is rejected\nby the runner and logged \u2014 the publish still returns 200 with its\n``blob_id``.", + "operationId": "publish_member_ai_api_ig_catalogs__catalog__members__member__post", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + }, + { + "name": "member", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Member" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_publish_member_ai_api_ig_catalogs__catalog__members__member__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgMemberBlobRef" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "Download Member", + "description": "Stream back a member's gzipped ``graph.json`` bytes; 404 if absent.", + "operationId": "download_member_ai_api_ig_catalogs__catalog__members__member__get", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + }, + { + "name": "member", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Member" + } + }, + { + "name": "sha", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sha" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/{catalog}/bundle": { + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "Download Bundle", + "description": "Stream the assembled catalog as a gzipped tarball for ``praxis ig sync``.\n\nThe ``ETag`` is the catalog version (from the stored summary). A caller that\nalready holds that version sends ``If-None-Match`` and gets a ``304`` with no\nbody, so an unchanged catalog is never re-downloaded.", + "operationId": "download_bundle_ai_api_ig_catalogs__catalog__bundle_get", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/{catalog}/infra/refresh": { + "post": { + "tags": [ + "ig-catalog" + ], + "summary": "Refresh Infra", + "description": "Rebuild this catalog's infra member (``ig infra build``) then re-assemble.\n\nThe infra member is the only member with no source repo, so no repo CI owns\nit; it must be rebuilt server-side (it needs live Facets credentials) on\nFacets deploy events and on a periodic safety net. This service does not\nreceive Facets deploy webhooks today, so this explicit endpoint is the manual\ntrigger (wiring a Facets deploy webhook to call it is a prerequisite for the\nevent-driven path).\n\nRuns in the background (``ig infra build`` is slow); the trigger surfaces any\nfailure \u2014 including absent Facets credentials, which the runner reports\nloudly before spawning ``ig`` \u2014 to the server logs.", + "operationId": "refresh_infra_ai_api_ig_catalogs__catalog__infra_refresh_post", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/infra/refresh": { + "post": { + "tags": [ + "ig-catalog" + ], + "summary": "Refresh Infra By Project", + "description": "Rebuild the infra member of every catalog keyed on a Facets project.\n\nThis is the event-driven trigger. The Facets control plane can already emit\na Facets-deploy webhook with no control-plane code change: an operator\ncreates one WEBHOOK notification channel whose ``authorizationHeader`` is\n``Bearer `` and subscribes ``APP_DEPLOYMENT`` to it, with a\nMustache ``payloadJson`` template that emits the deployed project. That\nchannel POSTs a single fixed URL, so this endpoint is *project*-keyed (not\ncatalog-keyed like the manual ops route above): the body carries the Facets\nproject and we fan out to every catalog in the caller's org whose manifest\nkeys its infra member on it.\n\nAuth + org scoping are identical to the other ig routes: ``validate_user_cookie``\nresolves the ``Authorization: Bearer `` the webhook sends and the\norg from the key's context. The fan-out never crosses orgs.\n\nResponse (always ``200``, even for a project no catalog names \u2014 a webhook\nfiring for an unrelated project must not error)::\n\n {\n \"project\": \"\",\n \"triggered\": [\"\", ...], # infra refresh started\n \"skipped\": [{\"catalog\": \"\", \"reason\": \"\"}]\n }\n\nWe reuse :func:`refresh_infra_for_catalog` (which reuses the assemble\nrunner's ``infra_refresh``) and await each attempt so the response reports\nthe real per-catalog outcome. A catalog whose assemble is already running is\nreported in ``skipped`` (its runner returns a non-success result rather than\nraising) \u2014 a normal outcome, never a 500.", + "operationId": "refresh_infra_by_project_ai_api_ig_infra_refresh_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgInfraRefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/ai-api/ig/catalogs/{catalog}/manifest": { + "post": { + "tags": [ + "ig-catalog" + ], + "summary": "Push Manifest", + "description": "Push a catalog's manifest text (push-only, no server-side editing).\n\nThe manifest is a small YAML file authored on a developer's laptop; ig\nnever owns its content. The server records the text verbatim plus stamps\n``pushed_by`` (the caller), ``pushed_at``, and the client-supplied\n``git_sha``. Backs ``praxis ig manifest push``.", + "operationId": "push_manifest_ai_api_ig_catalogs__catalog__manifest_post", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgManifestPushRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgManifest" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": [ + "ig-catalog" + ], + "summary": "Pull Manifest", + "description": "Pull a catalog's manifest; 404 if none has been pushed.\n\nBacks ``praxis ig manifest pull``, which repo CI calls with an API-key\nBearer token.", + "operationId": "pull_manifest_ai_api_ig_catalogs__catalog__manifest_get", + "parameters": [ + { + "name": "catalog", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Catalog" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IgManifest" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AccountStatus": { + "type": "string", + "enum": [ + "active", + "suspended", + "inactive" + ], + "title": "AccountStatus", + "description": "User account status" + }, + "Action": { + "properties": { + "type": { + "$ref": "#/components/schemas/ActionType", + "description": "Action type" + }, + "title": { + "type": "string", + "title": "Title", + "description": "Action title/summary" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "URL to the created resource (PR, issue, etc.)" + }, + "reason": { + "type": "string", + "title": "Reason", + "description": "Why this action was taken" + } + }, + "type": "object", + "required": [ + "type", + "title", + "reason" + ], + "title": "Action", + "description": "An action taken by a scheduled agent during a run." + }, + "ActionType": { + "type": "string", + "enum": [ + "PR", + "ISSUE", + "SLACK", + "SKIP" + ], + "title": "ActionType", + "description": "Type of action taken during a run" + }, + "ActivityContext": { + "properties": { + "schedule_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Schedule Id" + }, + "schedule_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Schedule Name" + }, + "channel_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel Id" + }, + "channel_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel Name" + }, + "thread_ts": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Thread Ts" + }, + "user_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Email" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + } + }, + "type": "object", + "title": "ActivityContext" + }, + "ActivityEntry": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "kind": { + "type": "string", + "title": "Kind" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Duration Ms" + }, + "status": { + "type": "string", + "title": "Status" + }, + "summary": { + "type": "string", + "maxLength": 200, + "title": "Summary", + "default": "" + }, + "cost_usd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost Usd" + }, + "findings_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Findings Count" + }, + "actions_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Actions Count" + }, + "context": { + "$ref": "#/components/schemas/ActivityContext" + } + }, + "type": "object", + "required": [ + "id", + "kind", + "agent_id", + "agent_name", + "timestamp", + "status" + ], + "title": "ActivityEntry" + }, + "ActivityResponse": { + "properties": { + "entries": { + "items": { + "$ref": "#/components/schemas/ActivityEntry" + }, + "type": "array", + "title": "Entries" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "type": "object", + "required": [ + "entries" + ], + "title": "ActivityResponse" + }, + "ActorTypeSummary": { + "properties": { + "actor_type": { + "type": "string", + "title": "Actor Type" + }, + "cost": { + "type": "number", + "title": "Cost" + }, + "event_count": { + "type": "integer", + "title": "Event Count" + } + }, + "type": "object", + "required": [ + "actor_type", + "cost", + "event_count" + ], + "title": "ActorTypeSummary" + }, + "AddLabelFilterRequest": { + "properties": { + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "type": "string", + "title": "Value" + }, + "operator": { + "type": "string", + "title": "Operator", + "default": "equals" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "title": "AddLabelFilterRequest" + }, + "AgentExportPackage": { + "properties": { + "version": { + "type": "string", + "title": "Version", + "default": "1.0" + }, + "exported_at": { + "type": "string", + "format": "date-time", + "title": "Exported At" + }, + "agent": { + "$ref": "#/components/schemas/CustomAgentExport" + }, + "mcp_servers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MCPServerExport" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Mcp Servers" + }, + "repository": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryExport" + }, + { + "type": "null" + } + ] + }, + "credentials_required": { + "items": { + "$ref": "#/components/schemas/CredentialReference" + }, + "type": "array", + "title": "Credentials Required" + } + }, + "type": "object", + "required": [ + "agent" + ], + "title": "AgentExportPackage", + "description": "Complete export package for a custom agent" + }, + "AgentMCPAttachmentCreate": { + "properties": { + "mcp_server_id": { + "type": "string", + "title": "Mcp Server Id", + "description": "MCP server ID to attach" + }, + "is_enabled": { + "type": "boolean", + "title": "Is Enabled", + "description": "Whether to enable this MCP initially", + "default": true + } + }, + "type": "object", + "required": [ + "mcp_server_id" + ], + "title": "AgentMCPAttachmentCreate", + "description": "Schema for creating a new agent MCP attachment" + }, + "AgentMCPAttachmentUpdate": { + "properties": { + "is_enabled": { + "type": "boolean", + "title": "Is Enabled", + "description": "Whether this MCP is enabled" + } + }, + "type": "object", + "required": [ + "is_enabled" + ], + "title": "AgentMCPAttachmentUpdate", + "description": "Schema for updating an existing attachment" + }, + "AgentModel": { + "type": "string", + "enum": [ + "intelligent", + "fast" + ], + "title": "AgentModel", + "description": "Available AI models for custom agents.\n\nModel IDs are configurable via environment variables:\n- ANTHROPIC_INTELLIGENT_MODEL (default: claude-sonnet-5)\n- ANTHROPIC_FAST_MODEL (default: claude-haiku-4-5-20251001)" + }, + "AgentRunResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "schedule_id": { + "type": "string", + "title": "Schedule Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "status": { + "type": "string", + "title": "Status" + }, + "started_at": { + "type": "string", + "format": "date-time", + "title": "Started At" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At" + }, + "findings": { + "items": { + "$ref": "#/components/schemas/Finding" + }, + "type": "array", + "title": "Findings" + }, + "actions": { + "items": { + "$ref": "#/components/schemas/Action" + }, + "type": "array", + "title": "Actions" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "tokens_used": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Tokens Used" + }, + "duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Duration Ms" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id" + }, + "prompt_snapshot": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Snapshot" + }, + "report_artifact_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Report Artifact Id" + } + }, + "type": "object", + "required": [ + "id", + "agent_id", + "schedule_id", + "organization_id", + "status", + "started_at" + ], + "title": "AgentRunResponse", + "description": "API response model for a scheduled agent run." + }, + "AgentScheduleCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Name" + }, + "display_name": { + "type": "string", + "maxLength": 100, + "title": "Display Name" + }, + "cron_expression": { + "type": "string", + "title": "Cron Expression" + }, + "timezone": { + "type": "string", + "title": "Timezone", + "default": "UTC" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true + }, + "objective": { + "type": "string", + "maxLength": 20000, + "minLength": 10, + "title": "Objective" + }, + "targets": { + "items": { + "$ref": "#/components/schemas/ScheduleTarget" + }, + "type": "array", + "title": "Targets" + }, + "slack_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SlackConfig" + }, + { + "type": "null" + } + ] + }, + "git_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/GitConfig" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "cron_expression", + "objective" + ], + "title": "AgentScheduleCreate", + "description": "Schema for creating a new agent schedule." + }, + "AgentScheduleResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "cron_expression": { + "type": "string", + "title": "Cron Expression" + }, + "timezone": { + "type": "string", + "title": "Timezone" + }, + "enabled": { + "type": "boolean", + "title": "Enabled" + }, + "objective": { + "type": "string", + "title": "Objective" + }, + "targets": { + "items": { + "$ref": "#/components/schemas/ScheduleTarget" + }, + "type": "array", + "title": "Targets" + }, + "slack_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SlackConfig" + }, + { + "type": "null" + } + ] + }, + "git_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/GitConfig" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "title": "Status" + }, + "consecutive_errors": { + "type": "integer", + "title": "Consecutive Errors" + }, + "last_run_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Run At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "open_findings_count": { + "type": "integer", + "title": "Open Findings Count", + "default": 0 + }, + "learnings_count": { + "type": "integer", + "title": "Learnings Count", + "default": 0 + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + } + }, + "type": "object", + "required": [ + "id", + "agent_id", + "organization_id", + "name", + "display_name", + "cron_expression", + "timezone", + "enabled", + "objective", + "status", + "consecutive_errors", + "created_by_email", + "created_at", + "updated_at" + ], + "title": "AgentScheduleResponse", + "description": "API response model for an agent schedule." + }, + "AgentScheduleUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "cron_expression": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cron Expression" + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone" + }, + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "objective": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ], + "title": "Objective" + }, + "targets": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ScheduleTarget" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Targets" + }, + "slack_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SlackConfig" + }, + { + "type": "null" + } + ] + }, + "git_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/GitConfig" + }, + { + "type": "null" + } + ] + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/ScheduleStatus" + }, + { + "type": "null" + } + ] + }, + "consecutive_errors": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Consecutive Errors" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags" + }, + "crystallized": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Crystallized" + } + }, + "type": "object", + "title": "AgentScheduleUpdate", + "description": "Schema for updating an existing agent schedule.\n\nPermissive on minimums where existing rows could violate Create-only\nconstraints (e.g. legacy schedules with short objectives)." + }, + "AgentWebhookCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Name" + }, + "display_name": { + "type": "string", + "maxLength": 100, + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "objective": { + "type": "string", + "maxLength": 20000, + "minLength": 10, + "title": "Objective" + }, + "target": { + "anyOf": [ + { + "$ref": "#/components/schemas/ScheduleTarget" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "agent_id", + "objective" + ], + "title": "AgentWebhookCreate", + "description": "Schema for creating an agent webhook." + }, + "AgentWebhookListResponse": { + "properties": { + "webhooks": { + "items": { + "$ref": "#/components/schemas/AgentWebhookResponse" + }, + "type": "array", + "title": "Webhooks" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "webhooks", + "total" + ], + "title": "AgentWebhookListResponse" + }, + "AgentWebhookResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Name" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "objective": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Objective" + }, + "target": { + "anyOf": [ + { + "$ref": "#/components/schemas/ScheduleTarget" + }, + { + "type": "null" + } + ] + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_triggered_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Triggered At" + }, + "invoke_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Invoke Url", + "description": "Public URL to POST to in order to fire this webhook. Anyone with this URL can fire it \u2014 treat it like a password." + }, + "webhook_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Webhook Token" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "agent_id", + "name", + "display_name", + "is_active", + "created_by_email", + "created_at", + "updated_at" + ], + "title": "AgentWebhookResponse", + "description": "API response for an agent webhook (enriched with joined names)." + }, + "AgentWebhookRunListResponse": { + "properties": { + "runs": { + "items": { + "$ref": "#/components/schemas/AgentWebhookRunResponse" + }, + "type": "array", + "title": "Runs" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "runs", + "total" + ], + "title": "AgentWebhookRunListResponse" + }, + "AgentWebhookRunResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "webhook_id": { + "type": "string", + "title": "Webhook Id" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "status": { + "$ref": "#/components/schemas/AgentWebhookRunStatus" + }, + "started_at": { + "type": "string", + "format": "date-time", + "title": "Started At" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At" + }, + "duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Duration Ms" + }, + "cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost" + }, + "tokens_used": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Tokens Used" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message" + }, + "session_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Key" + }, + "extra_instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Extra Instructions" + }, + "invoked_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Invoked By" + } + }, + "type": "object", + "required": [ + "id", + "webhook_id", + "agent_id", + "organization_id", + "status", + "started_at" + ], + "title": "AgentWebhookRunResponse", + "description": "API response with `id: str` (no _id alias), to keep JSON clean.\n\nMirrors routes/agent_runs.py::AgentRunResponse \u2014 flattens the store\nmodel so FastAPI/Pydantic doesn't emit `_id`." + }, + "AgentWebhookRunStatus": { + "type": "string", + "enum": [ + "running", + "success", + "failed" + ], + "title": "AgentWebhookRunStatus" + }, + "AgentWebhookUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id" + }, + "objective": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000, + "minLength": 10 + }, + { + "type": "null" + } + ], + "title": "Objective" + }, + "target": { + "anyOf": [ + { + "$ref": "#/components/schemas/ScheduleTarget" + }, + { + "type": "null" + } + ] + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + } + }, + "type": "object", + "title": "AgentWebhookUpdate", + "description": "Schema for updating an agent webhook. All fields optional." + }, + "AlertSource": { + "type": "string", + "enum": [ + "pagerduty", + "opsgenie", + "datadog", + "prometheus", + "slack", + "mattermost", + "teams", + "manual", + "webhook" + ], + "title": "AlertSource", + "description": "Source of the incident alert." + }, + "ApiKeyCreate": { + "properties": { + "key_name": { + "type": "string", + "title": "Key Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": [ + "key_name" + ], + "title": "ApiKeyCreate", + "description": "Data for creating a new API key" + }, + "ApiKeyCreateResponse": { + "properties": { + "api_key": { + "$ref": "#/components/schemas/ApiKeyResponse" + }, + "plaintext_key": { + "type": "string", + "title": "Plaintext Key", + "description": "The actual API key - store this securely, you won't see it again!" + } + }, + "type": "object", + "required": [ + "api_key", + "plaintext_key" + ], + "title": "ApiKeyCreateResponse", + "description": "Response when creating a new API key (includes plaintext key ONCE)" + }, + "ApiKeyResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "owner_email": { + "type": "string", + "title": "Owner Email" + }, + "key_name": { + "type": "string", + "title": "Key Name" + }, + "key_prefix": { + "type": "string", + "title": "Key Prefix" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At" + }, + "revoked_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Revoked At" + }, + "usage_count": { + "type": "integer", + "title": "Usage Count", + "default": 0 + }, + "last_used_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Used At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "can_edit": { + "type": "boolean", + "title": "Can Edit", + "description": "Whether current user can edit this API key", + "default": false + }, + "can_delete": { + "type": "boolean", + "title": "Can Delete", + "description": "Whether current user can delete this API key", + "default": false + }, + "is_revoked": { + "type": "boolean", + "title": "Is Revoked", + "description": "Whether this API key has been revoked", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "owner_email", + "key_name", + "key_prefix", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "ApiKeyResponse", + "description": "Response model for API key API (excludes key_hash for security)" + }, + "ApiKeyUpdate": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At" + } + }, + "type": "object", + "title": "ApiKeyUpdate", + "description": "Data for updating an existing API key" + }, + "AppConfigUpdateRequest": { + "properties": { + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config", + "description": "New configuration values" + } + }, + "type": "object", + "required": [ + "config" + ], + "title": "AppConfigUpdateRequest", + "description": "Request body for updating app configuration." + }, + "AppEnableRequest": { + "properties": { + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Config", + "description": "Initial configuration" + } + }, + "type": "object", + "title": "AppEnableRequest", + "description": "Request body for enabling an app." + }, + "AppIntegrations": { + "properties": { + "required": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required", + "description": "Integrations that must be configured before enabling" + }, + "optional": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Optional", + "description": "Integrations that enhance functionality but aren't required" + } + }, + "type": "object", + "title": "AppIntegrations", + "description": "Integration requirements for an app." + }, + "AppScopeConfig": { + "properties": { + "type": { + "type": "string", + "title": "Type", + "description": "Internal scope type name (workspace, project, account)" + }, + "label": { + "type": "string", + "title": "Label", + "description": "UI display label (Workspace, Project)" + }, + "plural": { + "type": "string", + "title": "Plural", + "description": "Plural form (Workspaces, Projects)" + }, + "icon": { + "type": "string", + "title": "Icon", + "description": "Ant Design icon name (TeamOutlined, FolderOutlined)" + }, + "create_label": { + "type": "string", + "title": "Create Label", + "description": "Create button label (New Workspace)" + } + }, + "type": "object", + "required": [ + "type", + "label", + "plural", + "icon", + "create_label" + ], + "title": "AppScopeConfig", + "description": "How this app organizes its data (workspace, project, etc.)." + }, + "AppWithStatus": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon" + }, + "category": { + "type": "string", + "enum": [ + "operations", + "development", + "analytics" + ], + "title": "Category" + }, + "route": { + "type": "string", + "title": "Route" + }, + "version": { + "type": "string", + "title": "Version" + }, + "scope": { + "$ref": "#/components/schemas/AppScopeConfig" + }, + "integrations": { + "$ref": "#/components/schemas/AppIntegrations" + }, + "beta": { + "type": "boolean", + "title": "Beta" + }, + "auth_modes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Auth Modes" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether enabled for this org", + "default": false + }, + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config", + "description": "Org-specific config" + } + }, + "type": "object", + "required": [ + "id", + "name", + "description", + "icon", + "category", + "route", + "version", + "scope", + "integrations", + "beta", + "auth_modes" + ], + "title": "AppWithStatus", + "description": "App metadata combined with organization-specific status." + }, + "AppsListResponse": { + "properties": { + "apps": { + "items": { + "$ref": "#/components/schemas/AppWithStatus" + }, + "type": "array", + "title": "Apps" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "apps", + "total" + ], + "title": "AppsListResponse", + "description": "Response for GET /apps endpoint." + }, + "Artifact": { + "properties": { + "_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Name" + }, + "created_by": { + "type": "string", + "title": "Created By" + }, + "name": { + "type": "string", + "title": "Name" + }, + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "format": { + "$ref": "#/components/schemas/ArtifactFormat" + }, + "mime": { + "type": "string", + "title": "Mime" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes" + }, + "content_ref": { + "type": "string", + "title": "Content Ref" + }, + "version": { + "type": "integer", + "title": "Version" + }, + "source": { + "$ref": "#/components/schemas/ArtifactSource" + }, + "session_id": { + "type": "string", + "title": "Session Id" + }, + "chat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Chat Id" + }, + "schedule_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Schedule Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "version_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Version Count" + } + }, + "type": "object", + "required": [ + "organization_id", + "agent_id", + "created_by", + "name", + "title", + "format", + "mime", + "size_bytes", + "content_ref", + "version", + "source", + "session_id", + "created_at" + ], + "title": "Artifact", + "description": "Artifact metadata document. Blob bytes live in GridFS, referenced by\n``content_ref``." + }, + "ArtifactFormat": { + "type": "string", + "enum": [ + "markdown", + "html" + ], + "title": "ArtifactFormat", + "description": "Rendered format of the artifact body." + }, + "ArtifactList": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/Artifact" + }, + "type": "array", + "title": "Items" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "ArtifactList" + }, + "ArtifactSource": { + "type": "string", + "enum": [ + "chat", + "schedule_run" + ], + "title": "ArtifactSource", + "description": "Where the artifact was emitted from." + }, + "ArtifactVersionItem": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "version": { + "type": "integer", + "title": "Version" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "title": { + "type": "string", + "title": "Title" + }, + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Name" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + } + }, + "type": "object", + "required": [ + "id", + "version", + "created_at", + "title", + "agent_id" + ], + "title": "ArtifactVersionItem", + "description": "Slim version-row projection for the versions endpoint." + }, + "ArtifactVersionList": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ArtifactVersionItem" + }, + "type": "array", + "title": "Items" + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "ArtifactVersionList" + }, + "AttachDetachRequest": { + "properties": { + "agent_name": { + "type": "string", + "title": "Agent Name" + } + }, + "type": "object", + "required": [ + "agent_name" + ], + "title": "AttachDetachRequest", + "description": "Body for attach/detach endpoints." + }, + "AuthStatus": { + "properties": { + "authenticated": { + "type": "boolean", + "title": "Authenticated" + }, + "auth_mode": { + "type": "string", + "title": "Auth Mode" + }, + "user": { + "anyOf": [ + { + "$ref": "#/components/schemas/AuthUserPublic" + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "authenticated", + "auth_mode" + ], + "title": "AuthStatus" + }, + "AuthUser": { + "properties": { + "user_id": { + "type": "string", + "title": "User Id" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + }, + "type": "object", + "required": [ + "user_id" + ], + "title": "AuthUser", + "description": "Strongly-typed user model for authentication and session context.\n\nNote: Use 'email' to uniquely identify a user. 'user_id' is for cost tracking\nand is shared across all users (emails) of the same customer." + }, + "AuthUserPublic": { + "properties": { + "user_id": { + "type": "string", + "title": "User Id" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "is_super_admin": { + "type": "boolean", + "title": "Is Super Admin", + "default": false + }, + "role": { + "type": "string", + "title": "Role", + "default": "member" + } + }, + "type": "object", + "required": [ + "user_id" + ], + "title": "AuthUserPublic", + "description": "Public schema for authenticated user (exposed via API)." + }, + "AutonomousUsageEntry": { + "properties": { + "actor_type": { + "type": "string", + "title": "Actor Type" + }, + "billing_user_id": { + "type": "string", + "title": "Billing User Id" + }, + "cost": { + "type": "number", + "title": "Cost" + }, + "event_count": { + "type": "integer", + "title": "Event Count" + } + }, + "type": "object", + "required": [ + "actor_type", + "billing_user_id", + "cost", + "event_count" + ], + "title": "AutonomousUsageEntry" + }, + "AwsCredentials": { + "properties": { + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "AWS provider", + "default": "aws" + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regions", + "description": "Regions to scan" + }, + "validated": { + "type": "boolean", + "title": "Validated", + "description": "Whether credentials validated", + "default": false + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error", + "description": "Error if validation failed" + }, + "role_arn": { + "type": "string", + "title": "Role Arn", + "description": "IAM Role ARN to assume" + }, + "external_id": { + "type": "string", + "title": "External Id", + "description": "External ID for AssumeRole" + } + }, + "type": "object", + "required": [ + "role_arn", + "external_id" + ], + "title": "AwsCredentials", + "description": "AWS credentials for cross-account access via IAM Role." + }, + "AzureCredentials": { + "properties": { + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "Azure provider", + "default": "azure" + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regions", + "description": "Regions to scan" + }, + "validated": { + "type": "boolean", + "title": "Validated", + "description": "Whether credentials validated", + "default": false + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error", + "description": "Error if validation failed" + }, + "tenant_id": { + "type": "string", + "title": "Tenant Id", + "description": "Azure AD Tenant ID" + }, + "client_id": { + "type": "string", + "title": "Client Id", + "description": "Service Principal App ID" + }, + "subscription_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Subscription Ids", + "description": "Azure Subscription IDs to scan" + } + }, + "type": "object", + "required": [ + "tenant_id", + "client_id" + ], + "title": "AzureCredentials", + "description": "Azure credentials for cross-subscription access via Service Principal." + }, + "BlueprintConfigBody": { + "properties": { + "businessNeed": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Businessneed" + }, + "active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Active" + }, + "cadence": { + "anyOf": [ + { + "$ref": "#/components/schemas/Cadence" + }, + { + "type": "null" + } + ] + }, + "writeEnabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Writeenabled" + }, + "clusters": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Clusters" + }, + "clusterNames": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Clusternames" + }, + "blueprintName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Blueprintname" + }, + "incidentWorkspaceUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Incidentworkspaceurl" + }, + "incidentApiKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Incidentapikey" + }, + "incidentUsername": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Incidentusername" + } + }, + "type": "object", + "title": "BlueprintConfigBody" + }, + "BlueprintEditRulesBody": { + "properties": { + "rules": { + "items": { + "$ref": "#/components/schemas/Rule" + }, + "type": "array", + "title": "Rules" + } + }, + "type": "object", + "required": [ + "rules" + ], + "title": "BlueprintEditRulesBody" + }, + "BlueprintRollbackBody": { + "properties": { + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + } + }, + "type": "object", + "title": "BlueprintRollbackBody" + }, + "BlueprintRunBody": { + "properties": { + "blueprintName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Blueprintname" + } + }, + "type": "object", + "title": "BlueprintRunBody" + }, + "Body_attach_repository_to_session_ai_api_repository_session_attach_post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "repository_id": { + "type": "string", + "title": "Repository Id" + }, + "auth_mode": { + "type": "string", + "title": "Auth Mode", + "default": "pat" + }, + "pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Id" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Branch", + "default": "main" + } + }, + "type": "object", + "required": [ + "session_id", + "repository_id" + ], + "title": "Body_attach_repository_to_session_ai_api_repository_session_attach_post" + }, + "Body_bulk_delete_boundaries_ai_api_migrations_projects__project_id__discovery_boundaries_bulk_delete_post": { + "properties": { + "boundary_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Boundary Ids" + } + }, + "type": "object", + "required": [ + "boundary_ids" + ], + "title": "Body_bulk_delete_boundaries_ai_api_migrations_projects__project_id__discovery_boundaries_bulk_delete_post" + }, + "Body_create_file_or_folder_ai_api_file_browser_create_post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "path": { + "type": "string", + "title": "Path" + }, + "is_directory": { + "type": "boolean", + "title": "Is Directory", + "default": false + } + }, + "type": "object", + "required": [ + "session_id", + "path" + ], + "title": "Body_create_file_or_folder_ai_api_file_browser_create_post" + }, + "Body_create_project_in_integration_ai_api_migrations_projects__project_id__discovery_facets_integrations__integration_id__projects_post": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name for the new project" + }, + "project_type": { + "type": "string", + "title": "Project Type", + "description": "Project type" + } + }, + "type": "object", + "required": [ + "name", + "project_type" + ], + "title": "Body_create_project_in_integration_ai_api_migrations_projects__project_id__discovery_facets_integrations__integration_id__projects_post" + }, + "Body_create_project_type_ai_api_integrations_facets__integration_id__project_types_post": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "Body_create_project_type_ai_api_integrations_facets__integration_id__project_types_post" + }, + "Body_detach_repository_from_session_ai_api_repository_session_detach_post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "repository_id": { + "type": "string", + "title": "Repository Id" + } + }, + "type": "object", + "required": [ + "session_id", + "repository_id" + ], + "title": "Body_detach_repository_from_session_ai_api_repository_session_detach_post" + }, + "Body_link_cloud_integration_ai_api_migrations_projects__project_id__discovery_link_integration_post": { + "properties": { + "cloud_source_id": { + "type": "string", + "title": "Cloud Source Id", + "description": "Cloud source integration ID" + } + }, + "type": "object", + "required": [ + "cloud_source_id" + ], + "title": "Body_link_cloud_integration_ai_api_migrations_projects__project_id__discovery_link_integration_post" + }, + "Body_link_facets_integration_ai_api_migrations_projects__project_id__discovery_facets_link_post": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "Facets integration ID" + }, + "integration_name": { + "type": "string", + "title": "Integration Name", + "description": "Integration name for display" + }, + "facets_project_id": { + "type": "string", + "title": "Facets Project Id", + "description": "Facets project/stack ID" + }, + "facets_project_name": { + "type": "string", + "title": "Facets Project Name", + "description": "Facets project name" + }, + "project_type": { + "type": "string", + "title": "Project Type", + "description": "Project type for module mapping" + } + }, + "type": "object", + "required": [ + "integration_id", + "facets_project_id", + "project_type" + ], + "title": "Body_link_facets_integration_ai_api_migrations_projects__project_id__discovery_facets_link_post" + }, + "Body_link_k8s_integration_ai_api_kubeconfig_link_integration__integration_id__post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "Body_link_k8s_integration_ai_api_kubeconfig_link_integration__integration_id__post" + }, + "Body_publish_member_ai_api_ig_catalogs__catalog__members__member__post": { + "properties": { + "graph": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "Graph", + "description": "gzipped graph.json bytes" + }, + "git": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git" + }, + "sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sha" + } + }, + "type": "object", + "required": [ + "graph" + ], + "title": "Body_publish_member_ai_api_ig_catalogs__catalog__members__member__post" + }, + "Body_set_kubeconfig_by_env_ai_api_kubeconfig_set_by_env__env_id__post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "Body_set_kubeconfig_by_env_ai_api_kubeconfig_set_by_env__env_id__post" + }, + "Body_set_migration_target_ai_api_migrations_projects__project_id__discovery_target_post": { + "properties": { + "target": { + "$ref": "#/components/schemas/MigrationTarget", + "description": "Target cloud: 'aws' or 'gcp'" + } + }, + "type": "object", + "required": [ + "target" + ], + "title": "Body_set_migration_target_ai_api_migrations_projects__project_id__discovery_target_post" + }, + "Body_upload_file_ai_api_files_upload_post": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "session_id", + "file" + ], + "title": "Body_upload_file_ai_api_files_upload_post" + }, + "Body_upload_file_ai_api_migrations_projects__project_id__files_post": { + "properties": { + "file": { + "type": "string", + "contentMediaType": "application/octet-stream", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_file_ai_api_migrations_projects__project_id__files_post" + }, + "BulkHelmApprovalRequest": { + "properties": { + "release_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Release Names" + }, + "approval": { + "type": "string", + "title": "Approval" + } + }, + "type": "object", + "required": [ + "release_names", + "approval" + ], + "title": "BulkHelmApprovalRequest" + }, + "Cadence": { + "type": "string", + "enum": [ + "auto", + "hourly", + "daily", + "weekly" + ], + "title": "Cadence", + "description": "'auto' ramps down over time; the rest are fixed intervals." + }, + "CancellationInfo": { + "properties": { + "is_scheduled": { + "type": "boolean", + "title": "Is Scheduled" + }, + "cancel_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cancel At" + }, + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": [ + "is_scheduled", + "status" + ], + "title": "CancellationInfo", + "description": "Cancellation status information." + }, + "CatalogEdgeResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id" + }, + "source_name": { + "type": "string", + "title": "Source Name" + }, + "target_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Id" + }, + "target_name": { + "type": "string", + "title": "Target Name" + }, + "edge_type": { + "type": "string", + "title": "Edge Type" + }, + "details": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Details" + }, + "inferred": { + "type": "boolean", + "title": "Inferred", + "default": true + }, + "insight": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Insight" + }, + "criticality": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Criticality" + }, + "confidence": { + "type": "number", + "title": "Confidence", + "default": 0.5 + }, + "confidence_source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Confidence Source" + }, + "last_verified_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Verified At" + }, + "discovered_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Discovered By" + }, + "environments": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Environments" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "source_name", + "target_name", + "edge_type", + "created_at", + "updated_at" + ], + "title": "CatalogEdgeResponse", + "description": "API response for a single edge." + }, + "CatalogEntryCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "resource_type": { + "type": "string", + "title": "Resource Type", + "default": "service" + }, + "source": { + "type": "string", + "title": "Source", + "default": "manual" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Tags" + } + }, + "type": "object", + "required": [ + "name", + "display_name" + ], + "title": "CatalogEntryCreate", + "description": "Request to create a catalog entry manually." + }, + "CatalogEntryResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "resource_type": { + "type": "string", + "title": "Resource Type" + }, + "source": { + "type": "string", + "title": "Source" + }, + "deployments": { + "items": { + "$ref": "#/components/schemas/DeploymentTarget" + }, + "type": "array", + "title": "Deployments" + }, + "app_repo": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepoRef" + }, + { + "type": "null" + } + ] + }, + "iac_repo": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepoRef" + }, + { + "type": "null" + } + ] + }, + "facets_intent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Intent" + }, + "facets_flavor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Flavor" + }, + "facets_project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project" + }, + "outgoing_edges": { + "items": { + "$ref": "#/components/schemas/CatalogEdgeResponse" + }, + "type": "array", + "title": "Outgoing Edges" + }, + "incoming_edges": { + "items": { + "$ref": "#/components/schemas/CatalogEdgeResponse" + }, + "type": "array", + "title": "Incoming Edges" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Tags" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "owner_team": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Owner Team" + }, + "insight": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Insight" + }, + "code_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code Summary" + }, + "system_role": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Role" + }, + "confidence": { + "type": "number", + "title": "Confidence", + "default": 0.5 + }, + "confidence_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Confidence Label" + }, + "completeness": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Completeness" + }, + "sources": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Sources" + }, + "cloud_identity": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Cloud Identity" + }, + "facets_identity": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Facets Identity" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Path" + }, + "parent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Parent Name" + }, + "parent_entry_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Parent Entry Id" + }, + "resource_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Category" + }, + "depth": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Depth" + }, + "insight_source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Insight Source" + }, + "grouping_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Grouping Key" + }, + "grouping_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Grouping Value" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by": { + "type": "string", + "title": "Created By", + "default": "" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "display_name", + "resource_type", + "source", + "created_at", + "updated_at" + ], + "title": "CatalogEntryResponse", + "description": "API response for a single catalog entry." + }, + "CatalogEntryUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "resource_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Type" + }, + "tags": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tags" + }, + "facets_intent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Intent" + }, + "facets_flavor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Flavor" + }, + "facets_project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project" + } + }, + "type": "object", + "title": "CatalogEntryUpdate", + "description": "Request to update a catalog entry." + }, + "CatalogListResponse": { + "properties": { + "entries": { + "items": { + "$ref": "#/components/schemas/CatalogEntryResponse" + }, + "type": "array", + "title": "Entries" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "entries", + "total" + ], + "title": "CatalogListResponse", + "description": "API response for listing catalog entries." + }, + "CatalogSummary": { + "properties": { + "total": { + "type": "integer", + "title": "Total", + "default": 0 + }, + "by_resource_type": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Resource Type" + }, + "by_source": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Source" + }, + "by_target_type": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Target Type" + }, + "total_connections": { + "type": "integer", + "title": "Total Connections", + "default": 0 + }, + "by_confidence_label": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Confidence Label" + }, + "by_completeness": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "By Completeness" + } + }, + "type": "object", + "title": "CatalogSummary", + "description": "Aggregate stats for the catalog." + }, + "ChangePlanRequest": { + "properties": { + "new_price_id": { + "type": "string", + "title": "New Price Id" + } + }, + "type": "object", + "required": [ + "new_price_id" + ], + "title": "ChangePlanRequest", + "description": "Request model for changing subscription plan" + }, + "ChatIntegrationCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-given name for this integration" + }, + "bot_token": { + "type": "string", + "title": "Bot Token", + "description": "Bot token (e.g., Slack xoxb-...)" + }, + "signing_secret": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signing Secret", + "description": "Signing secret for webhook verification (optional)" + } + }, + "type": "object", + "required": [ + "name", + "bot_token" + ], + "title": "ChatIntegrationCreate", + "description": "Schema for creating/connecting a Slack integration." + }, + "ChatIntegrationListResponse": { + "properties": { + "integrations": { + "items": { + "$ref": "#/components/schemas/ChatIntegrationResponse" + }, + "type": "array", + "title": "Integrations" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "integrations", + "total" + ], + "title": "ChatIntegrationListResponse", + "description": "API response for listing integrations." + }, + "ChatIntegrationResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "provider": { + "$ref": "#/components/schemas/ChatProvider" + }, + "name": { + "type": "string", + "title": "Name" + }, + "provider_workspace_id": { + "type": "string", + "title": "Provider Workspace Id" + }, + "provider_workspace_name": { + "type": "string", + "title": "Provider Workspace Name" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "usage_count": { + "type": "integer", + "title": "Usage Count", + "description": "Total number of channel configs and slack bots using this integration", + "default": 0 + }, + "workspace_usage_count": { + "type": "integer", + "title": "Workspace Usage Count", + "description": "Number of channel configs using this integration", + "default": 0 + }, + "bot_usage_count": { + "type": "integer", + "title": "Bot Usage Count", + "description": "Number of slack bots using this integration", + "default": 0 + }, + "has_bot_token": { + "type": "boolean", + "title": "Has Bot Token", + "description": "Whether bot token is configured", + "default": true + }, + "has_signing_secret": { + "type": "boolean", + "title": "Has Signing Secret", + "description": "Whether signing secret is configured", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "provider", + "name", + "provider_workspace_id", + "provider_workspace_name", + "is_active", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "ChatIntegrationResponse", + "description": "API response for chat integration." + }, + "ChatIntegrationUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "New name for the integration" + }, + "bot_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bot Token", + "description": "New bot token (will be validated and encrypted)" + }, + "signing_secret": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signing Secret", + "description": "New signing secret (will be encrypted)" + }, + "clear_signing_secret": { + "type": "boolean", + "title": "Clear Signing Secret", + "description": "Set to true to remove the signing secret", + "default": false + } + }, + "type": "object", + "title": "ChatIntegrationUpdate", + "description": "Schema for updating a chat integration." + }, + "ChatProvider": { + "type": "string", + "enum": [ + "slack", + "mattermost", + "teams", + "discord" + ], + "title": "ChatProvider", + "description": "Chat platform provider." + }, + "CheckoutSessionRequest": { + "properties": { + "price_id": { + "type": "string", + "title": "Price Id" + } + }, + "type": "object", + "required": [ + "price_id" + ], + "title": "CheckoutSessionRequest", + "description": "Request model for creating a checkout session" + }, + "CheckoutSessionResponse": { + "properties": { + "checkout_url": { + "type": "string", + "title": "Checkout Url" + } + }, + "type": "object", + "required": [ + "checkout_url" + ], + "title": "CheckoutSessionResponse", + "description": "Response model with checkout URL" + }, + "CliSessionKeyRequest": { + "properties": { + "plaintext_key": { + "type": "string", + "maxLength": 512, + "minLength": 8, + "title": "Plaintext Key" + } + }, + "type": "object", + "required": [ + "plaintext_key" + ], + "title": "CliSessionKeyRequest" + }, + "CliSessionKeyResponse": { + "properties": { + "plaintext_key": { + "type": "string", + "title": "Plaintext Key" + } + }, + "type": "object", + "required": [ + "plaintext_key" + ], + "title": "CliSessionKeyResponse" + }, + "CloudImportRequest": { + "properties": { + "control_plane_account_id": { + "type": "string", + "title": "Control Plane Account Id" + }, + "source_type": { + "$ref": "#/components/schemas/CloudSourceType" + }, + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Name" + }, + "region": { + "type": "string", + "minLength": 1, + "title": "Region" + }, + "regions": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Regions", + "description": "GCP only \u2014 optional additional regions" + } + }, + "type": "object", + "required": [ + "control_plane_account_id", + "source_type", + "name", + "region" + ], + "title": "CloudImportRequest", + "description": "Request body for importing a cloud account from the control plane." + }, + "CloudIntegrationInfo": { + "properties": { + "cloud_source_id": { + "type": "string", + "title": "Cloud Source Id", + "description": "FK to cloud_sources.source_id" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Integration display name" + }, + "source_type": { + "type": "string", + "title": "Source Type", + "description": "aws | gcp | azure" + }, + "validated": { + "type": "boolean", + "title": "Validated", + "description": "Whether integration is validated", + "default": false + }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Metadata", + "description": "Display metadata (e.g. role_arn, region)" + } + }, + "type": "object", + "required": [ + "cloud_source_id", + "name", + "source_type" + ], + "title": "CloudIntegrationInfo", + "description": "Denormalized cloud integration info for API response." + }, + "CloudProvider": { + "type": "string", + "enum": [ + "aws", + "azure", + "gcp" + ], + "title": "CloudProvider", + "description": "Supported cloud providers for discovery." + }, + "CloudSetupInfo": { + "properties": { + "identity_mode": { + "type": "string", + "title": "Identity Mode", + "description": "Server identity mode: 'aws' or 'gcp'" + }, + "aws_account_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Account Id", + "description": "Our AWS account ID (AWS mode only)" + }, + "external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Id", + "description": "Organization-scoped external ID (AWS mode only)" + }, + "gcp_sa_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sa Email", + "description": "GCP Service Account email (GCP mode only)" + }, + "gcp_sa_unique_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sa Unique Id", + "description": "GCP SA unique ID / sub claim (GCP mode only)" + }, + "google_thumbprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Thumbprint", + "description": "SHA1 thumbprint of accounts.google.com TLS cert (GCP mode only)" + } + }, + "type": "object", + "required": [ + "identity_mode" + ], + "title": "CloudSetupInfo", + "description": "Information needed for cross-account setup (AWS or GCP server identity)." + }, + "CloudSourceCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Name" + }, + "source_type": { + "$ref": "#/components/schemas/CloudSourceType" + }, + "credentials": { + "additionalProperties": true, + "type": "object", + "title": "Credentials", + "description": "Plain credentials (encrypted before storage)" + } + }, + "type": "object", + "required": [ + "name", + "source_type", + "credentials" + ], + "title": "CloudSourceCreate", + "description": "Request body for creating a cloud source." + }, + "CloudSourceListResponse": { + "properties": { + "integrations": { + "items": { + "$ref": "#/components/schemas/CloudSourceResponse" + }, + "type": "array", + "title": "Integrations" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "integrations", + "total" + ], + "title": "CloudSourceListResponse", + "description": "Response for listing cloud integrations." + }, + "CloudSourceResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "source_id": { + "type": "string", + "title": "Source Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "source_type": { + "$ref": "#/components/schemas/CloudSourceType" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Metadata" + }, + "validated": { + "type": "boolean", + "title": "Validated" + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "usage_count": { + "type": "integer", + "title": "Usage Count", + "default": 0 + } + }, + "type": "object", + "required": [ + "id", + "source_id", + "organization_id", + "name", + "source_type", + "is_active", + "metadata", + "validated", + "created_at", + "updated_at" + ], + "title": "CloudSourceResponse", + "description": "API response for a cloud source - NEVER includes encrypted_credentials." + }, + "CloudSourceType": { + "type": "string", + "enum": [ + "aws", + "gcp", + "azure" + ], + "title": "CloudSourceType" + }, + "CloudSourceUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string", + "maxLength": 200, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + }, + "credentials": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Credentials", + "description": "New credentials (will be re-validated and encrypted)" + } + }, + "type": "object", + "title": "CloudSourceUpdate", + "description": "Request body for updating a cloud source (name, active status, or credentials)." + }, + "CommChannelResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "platform": { + "$ref": "#/components/schemas/ChatProvider" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "channel_id": { + "type": "string", + "title": "Channel Id" + }, + "channel_name": { + "type": "string", + "title": "Channel Name" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name", + "description": "Denormalized: name of the linked ChatIntegration" + }, + "provider_workspace_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Workspace Name", + "description": "Denormalized: provider workspace name (e.g. Slack team)" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "workspace_id", + "platform", + "integration_id", + "channel_id", + "channel_name", + "created_at", + "updated_at" + ], + "title": "CommChannelResponse", + "description": "API response for a comm channel." + }, + "CommChannelUpsert": { + "properties": { + "platform": { + "$ref": "#/components/schemas/ChatProvider", + "description": "Chat platform" + }, + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "ChatIntegration ID to use" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Channel ID to post to" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel name for display" + } + }, + "type": "object", + "required": [ + "platform", + "integration_id", + "channel_id", + "channel_name" + ], + "title": "CommChannelUpsert", + "description": "Schema for setting/replacing a workspace's comm channel." + }, + "ConnectIntegrationRequest": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name" + } + }, + "type": "object", + "required": [ + "integration_id" + ], + "title": "ConnectIntegrationRequest" + }, + "ConsolidateRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "ConsolidateRequest" + }, + "ConsolidateStats": { + "properties": { + "files_seen": { + "type": "integer", + "title": "Files Seen", + "default": 0 + }, + "unchanged": { + "type": "integer", + "title": "Unchanged", + "default": 0 + }, + "modified": { + "type": "integer", + "title": "Modified", + "default": 0 + }, + "new": { + "type": "integer", + "title": "New", + "default": 0 + }, + "deleted": { + "type": "integer", + "title": "Deleted", + "default": 0 + }, + "cost_usd": { + "type": "number", + "title": "Cost Usd", + "default": 0.0 + }, + "input_tokens": { + "type": "integer", + "title": "Input Tokens", + "default": 0 + }, + "output_tokens": { + "type": "integer", + "title": "Output Tokens", + "default": 0 + } + }, + "type": "object", + "title": "ConsolidateStats", + "description": "Per-call stats \u2014 returned to caller for logging + cost reporting." + }, + "ControlPlaneAccountSummary": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "source_type": { + "$ref": "#/components/schemas/CloudSourceType" + }, + "display_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Hint" + }, + "already_imported": { + "type": "boolean", + "title": "Already Imported", + "default": false + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region" + } + }, + "type": "object", + "required": [ + "id", + "name", + "source_type" + ], + "title": "ControlPlaneAccountSummary", + "description": "A control-plane cloud account, slimmed for the import picker." + }, + "ControlPlaneAvailabilityResponse": { + "properties": { + "available": { + "type": "boolean", + "title": "Available" + } + }, + "type": "object", + "required": [ + "available" + ], + "title": "ControlPlaneAvailabilityResponse", + "description": "Whether the 'Import from Facets' flow is enabled on this deployment." + }, + "CreateFacetsProjectRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "project_type": { + "type": "string", + "title": "Project Type" + } + }, + "type": "object", + "required": [ + "name", + "project_type" + ], + "title": "CreateFacetsProjectRequest" + }, + "CredentialReference": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "used_by": { + "type": "string", + "title": "Used By" + } + }, + "type": "object", + "required": [ + "name", + "used_by" + ], + "title": "CredentialReference", + "description": "Credential reference info for import warnings" + }, + "CredentialResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "owner_email": { + "type": "string", + "title": "Owner Email" + }, + "credential_name": { + "type": "string", + "title": "Credential Name" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Service" + }, + "scope": { + "$ref": "#/components/schemas/CredentialScope" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Metadata" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At" + }, + "last_used_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Used At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "can_edit": { + "type": "boolean", + "title": "Can Edit", + "description": "Whether current user can edit this credential", + "default": false + }, + "can_delete": { + "type": "boolean", + "title": "Can Delete", + "description": "Whether current user can delete this credential", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "owner_email", + "credential_name", + "scope", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "CredentialResponse", + "description": "Response model for credential API (excludes encrypted_value for security)" + }, + "CredentialScope": { + "type": "string", + "enum": [ + "USER", + "ORGANIZATION" + ], + "title": "CredentialScope", + "description": "Scope of credential visibility" + }, + "CredentialSubmitRequest": { + "properties": { + "credential_name": { + "type": "string", + "title": "Credential Name" + }, + "value": { + "type": "string", + "title": "Value" + }, + "service": { + "type": "string", + "title": "Service" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "scope": { + "type": "string", + "title": "Scope", + "default": "USER" + } + }, + "type": "object", + "required": [ + "credential_name", + "value", + "service" + ], + "title": "CredentialSubmitRequest", + "description": "Request model for credential submission" + }, + "CredentialUpdate": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + }, + "expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Expires At" + } + }, + "type": "object", + "title": "CredentialUpdate", + "description": "Data for updating an existing credential" + }, + "CurMappingStatus": { + "type": "string", + "enum": [ + "draft", + "uploading", + "analyzing", + "completed", + "error" + ], + "title": "CurMappingStatus", + "description": "CUR mapping analysis status." + }, + "CustomAgentCreate": { + "properties": { + "scope": { + "$ref": "#/components/schemas/CustomAgentScope", + "description": "Agent scope", + "default": "organization" + }, + "name": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "pattern": "^[a-z0-9-]+$", + "title": "Name" + }, + "display_name": { + "type": "string", + "maxLength": 100, + "title": "Display Name" + }, + "description": { + "type": "string", + "maxLength": 500, + "minLength": 10, + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon", + "default": "bot" + }, + "goal": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ], + "title": "Goal" + }, + "triggers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Triggers" + }, + "model": { + "$ref": "#/components/schemas/AgentModel", + "default": "intelligent" + }, + "system_prompt": { + "type": "string", + "maxLength": 100000, + "minLength": 50, + "title": "System Prompt" + }, + "enabled_system_mcps": { + "items": { + "$ref": "#/components/schemas/SystemMCP" + }, + "type": "array", + "title": "Enabled System Mcps" + }, + "attached_custom_mcp_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Custom Mcp Ids" + }, + "attached_databases": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Databases" + }, + "repository_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryConfig" + }, + { + "type": "null" + } + ] + }, + "kubernetes_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/KubernetesConfig" + }, + { + "type": "null" + } + ] + }, + "enable_reasoning_output": { + "type": "boolean", + "title": "Enable Reasoning Output", + "default": true + }, + "chat_enabled": { + "type": "boolean", + "title": "Chat Enabled", + "default": true + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "system_prompt" + ], + "title": "CustomAgentCreate", + "description": "Schema for creating a new custom agent" + }, + "CustomAgentExport": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon", + "default": "bot" + }, + "goal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Goal" + }, + "triggers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Triggers" + }, + "model": { + "$ref": "#/components/schemas/AgentModel" + }, + "system_prompt": { + "type": "string", + "title": "System Prompt" + }, + "enabled_system_mcps": { + "items": { + "$ref": "#/components/schemas/SystemMCP" + }, + "type": "array", + "title": "Enabled System Mcps" + }, + "repository_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryConfigExport" + }, + { + "type": "null" + } + ] + }, + "kubernetes_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/KubernetesConfigExport" + }, + { + "type": "null" + } + ] + }, + "enable_reasoning_output": { + "type": "boolean", + "title": "Enable Reasoning Output", + "default": true + }, + "chat_enabled": { + "type": "boolean", + "title": "Chat Enabled", + "default": true + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "model", + "system_prompt" + ], + "title": "CustomAgentExport", + "description": "Custom agent data for export (portable, no IDs)" + }, + "CustomAgentResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "scope": { + "$ref": "#/components/schemas/CustomAgentScope" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "owner_email": { + "type": "string", + "title": "Owner Email" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon" + }, + "goal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Goal" + }, + "triggers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Triggers" + }, + "model": { + "$ref": "#/components/schemas/AgentModel" + }, + "system_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Prompt" + }, + "enabled_system_mcps": { + "items": { + "$ref": "#/components/schemas/SystemMCP" + }, + "type": "array", + "title": "Enabled System Mcps" + }, + "attached_custom_mcp_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Custom Mcp Ids" + }, + "attached_databases": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Databases", + "description": "IDs of databases attached to this agent" + }, + "repository_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryConfig" + }, + { + "type": "null" + } + ] + }, + "kubernetes_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/KubernetesConfig" + }, + { + "type": "null" + } + ] + }, + "enable_reasoning_output": { + "type": "boolean", + "title": "Enable Reasoning Output" + }, + "chat_enabled": { + "type": "boolean", + "title": "Chat Enabled", + "default": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "has_database": { + "type": "boolean", + "title": "Has Database", + "description": "Convenience flag: true iff attached_databases is non-empty. Kept for back-compat with UI code that still reads this field.", + "default": false + }, + "schedule_count": { + "type": "integer", + "title": "Schedule Count", + "description": "Number of schedules configured for this agent", + "default": 0 + }, + "attached_mcp_count": { + "type": "integer", + "title": "Attached Mcp Count", + "description": "Number of MCP servers the current user has attached", + "default": 0 + }, + "can_edit": { + "type": "boolean", + "title": "Can Edit", + "description": "Can current user edit this agent" + }, + "can_delete": { + "type": "boolean", + "title": "Can Delete", + "description": "Can current user delete this agent" + } + }, + "type": "object", + "required": [ + "id", + "scope", + "organization_id", + "owner_email", + "name", + "display_name", + "description", + "icon", + "model", + "enabled_system_mcps", + "attached_custom_mcp_ids", + "repository_config", + "kubernetes_config", + "enable_reasoning_output", + "created_at", + "updated_at", + "is_active", + "can_edit", + "can_delete" + ], + "title": "CustomAgentResponse", + "description": "Schema for custom agent API responses" + }, + "CustomAgentScope": { + "type": "string", + "enum": [ + "global", + "organization", + "personal" + ], + "title": "CustomAgentScope", + "description": "Scope of custom agent" + }, + "CustomAgentUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500, + "minLength": 10 + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon" + }, + "goal": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ], + "title": "Goal" + }, + "triggers": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Triggers" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentModel" + }, + { + "type": "null" + } + ] + }, + "system_prompt": { + "anyOf": [ + { + "type": "string", + "maxLength": 100000 + }, + { + "type": "null" + } + ], + "title": "System Prompt" + }, + "enabled_system_mcps": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/SystemMCP" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Enabled System Mcps" + }, + "attached_custom_mcp_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attached Custom Mcp Ids" + }, + "attached_databases": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Attached Databases" + }, + "repository_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryConfig" + }, + { + "type": "null" + } + ] + }, + "kubernetes_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/KubernetesConfig" + }, + { + "type": "null" + } + ] + }, + "chat_enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Chat Enabled" + } + }, + "type": "object", + "title": "CustomAgentUpdate", + "description": "Schema for updating an existing custom agent" + }, + "CustomToolDetail": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "status": { + "type": "string", + "title": "Status", + "default": "draft" + }, + "scope": { + "type": "string", + "title": "Scope", + "default": "organization" + }, + "required_credentials": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required Credentials" + }, + "version": { + "type": "integer", + "title": "Version", + "default": 1 + }, + "created_by_email": { + "type": "string", + "title": "Created By Email", + "default": "" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created At" + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Updated At" + }, + "attached_agents": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Agents", + "description": "Names of agents this tool is attached to" + }, + "python_code": { + "type": "string", + "title": "Python Code", + "default": "" + }, + "input_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Input Schema" + }, + "execution_config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Execution Config" + }, + "test_results": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Test Results" + }, + "allowed_imports": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Allowed Imports" + }, + "credential_descriptions": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Credential Descriptions" + }, + "organization_id": { + "type": "string", + "title": "Organization Id", + "default": "" + }, + "analytics": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolAnalytics" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "CustomToolDetail", + "description": "Full tool detail including code and schemas." + }, + "CycleEntry": { + "properties": { + "cycle_key": { + "type": "string", + "title": "Cycle Key" + }, + "start_date": { + "type": "string", + "title": "Start Date" + }, + "end_date": { + "type": "string", + "title": "End Date" + }, + "is_current": { + "type": "boolean", + "title": "Is Current" + } + }, + "type": "object", + "required": [ + "cycle_key", + "start_date", + "end_date", + "is_current" + ], + "title": "CycleEntry" + }, + "CyclesResponse": { + "properties": { + "cycle_day": { + "type": "integer", + "title": "Cycle Day" + }, + "cycles": { + "items": { + "$ref": "#/components/schemas/CycleEntry" + }, + "type": "array", + "title": "Cycles" + } + }, + "type": "object", + "required": [ + "cycle_day", + "cycles" + ], + "title": "CyclesResponse" + }, + "DailyTrend": { + "properties": { + "date": { + "type": "string", + "title": "Date" + }, + "count": { + "type": "integer", + "title": "Count" + }, + "resolved": { + "type": "integer", + "title": "Resolved" + } + }, + "type": "object", + "required": [ + "date", + "count", + "resolved" + ], + "title": "DailyTrend", + "description": "Daily incident count." + }, + "DailyUsageEntry": { + "properties": { + "date": { + "type": "string", + "title": "Date" + }, + "cost": { + "type": "number", + "title": "Cost" + }, + "event_count": { + "type": "integer", + "title": "Event Count" + } + }, + "type": "object", + "required": [ + "date", + "cost", + "event_count" + ], + "title": "DailyUsageEntry" + }, + "DailyUsageResponse": { + "properties": { + "cycle_key": { + "type": "string", + "title": "Cycle Key" + }, + "days": { + "items": { + "$ref": "#/components/schemas/DailyUsageEntry" + }, + "type": "array", + "title": "Days" + } + }, + "type": "object", + "required": [ + "cycle_key", + "days" + ], + "title": "DailyUsageResponse" + }, + "DatabaseCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9_]+$", + "title": "Name", + "description": "URL-safe identifier (used in MCP tool names when 2+ DBs attached)" + }, + "display_name": { + "type": "string", + "maxLength": 100, + "title": "Display Name" + }, + "description": { + "type": "string", + "maxLength": 500, + "title": "Description", + "default": "" + } + }, + "type": "object", + "required": [ + "name", + "display_name" + ], + "title": "DatabaseCreate", + "description": "Schema for creating a new database." + }, + "DatabaseResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "schema_description": { + "type": "string", + "title": "Schema Description" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "version": { + "type": "integer", + "title": "Version" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes" + }, + "last_modified_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Modified At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "attached_agent_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attached Agent Ids", + "description": "Agents currently attached to this DB (filled by route handler)" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "display_name", + "description", + "schema_description", + "created_by_email", + "version", + "size_bytes", + "last_modified_at", + "created_at", + "updated_at", + "is_active" + ], + "title": "DatabaseResponse", + "description": "Schema for database API responses." + }, + "DatabaseUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "title": "DatabaseUpdate", + "description": "Schema for updating an existing database (metadata only \u2014 bytes are\nmanaged via the MCP `execute_sql` flow)." + }, + "DeploymentTarget": { + "properties": { + "target_type": { + "type": "string", + "title": "Target Type" + }, + "environment_label": { + "type": "string", + "title": "Environment Label" + }, + "integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Id" + }, + "workspace_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id" + }, + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config" + }, + "last_scanned_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Scanned At" + }, + "scan_status": { + "type": "string", + "title": "Scan Status", + "default": "pending" + }, + "scan_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scan Error" + }, + "declared_config": { + "additionalProperties": true, + "type": "object", + "title": "Declared Config" + }, + "drift_detected": { + "type": "boolean", + "title": "Drift Detected", + "default": false + }, + "drift_details": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Drift Details" + }, + "insight": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Insight" + } + }, + "type": "object", + "required": [ + "target_type", + "environment_label" + ], + "title": "DeploymentTarget", + "description": "One deployment of a resource in a specific environment/compute." + }, + "DeploymentTargetCreate": { + "properties": { + "target_type": { + "type": "string", + "title": "Target Type" + }, + "environment_label": { + "type": "string", + "title": "Environment Label" + }, + "integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Id" + }, + "workspace_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id" + }, + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config" + } + }, + "type": "object", + "required": [ + "target_type", + "environment_label" + ], + "title": "DeploymentTargetCreate", + "description": "Request to add a deployment target." + }, + "DevelopModuleRequest": { + "properties": { + "intent": { + "type": "string", + "title": "Intent" + }, + "flavor": { + "type": "string", + "title": "Flavor" + } + }, + "type": "object", + "required": [ + "intent", + "flavor" + ], + "title": "DevelopModuleRequest" + }, + "DiscoverRequest": { + "properties": { + "url": { + "type": "string", + "title": "Url" + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "DiscoverRequest" + }, + "DiscoveredRepository": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "provider": { + "$ref": "#/components/schemas/GitProvider" + }, + "is_private": { + "type": "boolean", + "title": "Is Private" + }, + "default_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Branch" + }, + "can_add": { + "type": "boolean", + "title": "Can Add", + "description": "Whether this repo can be added (not duplicate)" + } + }, + "type": "object", + "required": [ + "name", + "url", + "provider", + "is_private", + "can_add" + ], + "title": "DiscoveredRepository", + "description": "Model for a discovered repository" + }, + "DiscoveredResource": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Resource name (e.g., 'my-eks-cluster')" + }, + "resource_id": { + "type": "string", + "title": "Resource Id", + "description": "Cloud resource ID (e.g., ARN, resource ID)" + }, + "region": { + "type": "string", + "title": "Region", + "description": "Region where resource exists" + }, + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config", + "description": "Resource configuration extracted from cloud" + } + }, + "type": "object", + "required": [ + "name", + "resource_id", + "region" + ], + "title": "DiscoveredResource", + "description": "A single discovered cloud resource with its identifiers and configuration." + }, + "DiscoveryAwsLinkRequest": { + "properties": { + "role_arn": { + "type": "string", + "title": "Role Arn", + "description": "IAM Role ARN to assume" + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regions", + "description": "AWS regions to scan" + }, + "external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Id", + "description": "External ID for AssumeRole (uses pre-generated if not provided)" + } + }, + "type": "object", + "required": [ + "role_arn" + ], + "title": "DiscoveryAwsLinkRequest", + "description": "Request to link AWS account." + }, + "DiscoveryBoundary": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "boundary_type": { + "type": "string", + "title": "Boundary Type", + "description": "'vpc' or 'tag'" + }, + "value": { + "type": "string", + "title": "Value", + "description": "VPC ID or 'key=value' for tags" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Human-readable name" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region (null for tags)" + }, + "resource_count": { + "type": "integer", + "title": "Resource Count", + "description": "Estimated resource count", + "default": 0 + }, + "included": { + "type": "boolean", + "title": "Included", + "description": "Whether to include in scan", + "default": true + }, + "partial": { + "type": "boolean", + "title": "Partial", + "description": "True when tag scan was truncated; value list may be incomplete", + "default": false + }, + "partial_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Partial Reason", + "description": "Truncation reason from paginator, persisted for diagnostics" + } + }, + "type": "object", + "required": [ + "boundary_type", + "value", + "display_name" + ], + "title": "DiscoveryBoundary", + "description": "VPC or tag boundary for filtering resources." + }, + "DiscoveryBoundaryCreate": { + "properties": { + "boundary_type": { + "type": "string", + "title": "Boundary Type", + "description": "'vpc', 'vnet', 'tag', or 'resource_group'" + }, + "value": { + "type": "string", + "title": "Value", + "description": "VPC ID, VNet ID, tag 'key=value', or resource group name" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Human-readable name for display" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region (optional for tags)" + }, + "resource_count": { + "type": "integer", + "title": "Resource Count", + "description": "Estimated resource count", + "default": 0 + }, + "included": { + "type": "boolean", + "title": "Included", + "description": "Whether to include in scan", + "default": true + } + }, + "type": "object", + "required": [ + "boundary_type", + "value", + "display_name" + ], + "title": "DiscoveryBoundaryCreate", + "description": "Request to add a new boundary manually." + }, + "DiscoveryBoundaryUpdate": { + "properties": { + "boundary_id": { + "type": "string", + "title": "Boundary Id" + }, + "included": { + "type": "boolean", + "title": "Included" + } + }, + "type": "object", + "required": [ + "boundary_id", + "included" + ], + "title": "DiscoveryBoundaryUpdate", + "description": "Request to update boundary inclusion." + }, + "DiscoveryConfigResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "project_id": { + "type": "string", + "title": "Project Id" + }, + "status": { + "$ref": "#/components/schemas/DiscoveryStatus" + }, + "boundary_scan_status": { + "$ref": "#/components/schemas/DiscoveryStatus", + "default": "not_started" + }, + "resource_scan_status": { + "$ref": "#/components/schemas/DiscoveryStatus", + "default": "not_started" + }, + "cloud_source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cloud Source Id" + }, + "cloud_integration": { + "anyOf": [ + { + "$ref": "#/components/schemas/CloudIntegrationInfo" + }, + { + "type": "null" + } + ] + }, + "cloud_provider": { + "anyOf": [ + { + "$ref": "#/components/schemas/CloudProvider" + }, + { + "type": "null" + } + ] + }, + "aws_credentials": { + "anyOf": [ + { + "$ref": "#/components/schemas/AwsCredentials" + }, + { + "type": "null" + } + ] + }, + "azure_credentials": { + "anyOf": [ + { + "$ref": "#/components/schemas/AzureCredentials" + }, + { + "type": "null" + } + ] + }, + "gcp_credentials": { + "anyOf": [ + { + "$ref": "#/components/schemas/GcpCredentials" + }, + { + "type": "null" + } + ] + }, + "linked_repository_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Linked Repository Id" + }, + "linked_pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Linked Pat Id" + }, + "repository_skipped": { + "type": "boolean", + "title": "Repository Skipped", + "default": false + }, + "boundaries": { + "items": { + "$ref": "#/components/schemas/DiscoveryBoundary" + }, + "type": "array", + "title": "Boundaries", + "default": [] + }, + "tag_match_mode": { + "type": "string", + "enum": [ + "any", + "all" + ], + "title": "Tag Match Mode", + "default": "any" + }, + "tag_key_operators": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Tag Key Operators", + "default": {} + }, + "resource_type_counts": { + "items": { + "$ref": "#/components/schemas/ResourceTypeCount" + }, + "type": "array", + "title": "Resource Type Counts", + "default": [] + }, + "facets": { + "anyOf": [ + { + "$ref": "#/components/schemas/FacetsConfig" + }, + { + "type": "null" + } + ] + }, + "migration_target": { + "anyOf": [ + { + "$ref": "#/components/schemas/MigrationTarget" + }, + { + "type": "null" + } + ] + }, + "type_mappings": { + "items": { + "$ref": "#/components/schemas/ResourceTypeMapping" + }, + "type": "array", + "title": "Type Mappings", + "default": [] + }, + "scan_truncated": { + "type": "boolean", + "title": "Scan Truncated", + "default": false + }, + "scan_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scan Error" + }, + "last_scan_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Scan At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "scan_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Scan Session Id" + }, + "resource_scan_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Scan Session Id" + } + }, + "type": "object", + "required": [ + "id", + "project_id", + "status", + "created_at", + "updated_at" + ], + "title": "DiscoveryConfigResponse", + "description": "API response for discovery config (multi-cloud)." + }, + "DiscoveryFacetsLinkRequest": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "Facets integration ID (org-level)" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name", + "description": "Integration name for display" + }, + "project_id": { + "type": "string", + "title": "Project Id", + "description": "Facets project/stack ID" + }, + "project_name": { + "type": "string", + "title": "Project Name", + "description": "Facets project/stack name" + }, + "project_type": { + "type": "string", + "title": "Project Type", + "description": "Project type for module mapping" + }, + "environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Id", + "description": "Facets environment/cluster ID" + }, + "environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Name", + "description": "Facets environment/cluster name" + } + }, + "type": "object", + "required": [ + "integration_id", + "project_id", + "project_name", + "project_type" + ], + "title": "DiscoveryFacetsLinkRequest", + "description": "Request to link Facets integration and project (before resource discovery)." + }, + "DiscoverySetupInfo": { + "properties": { + "identity_mode": { + "type": "string", + "title": "Identity Mode", + "description": "Server identity mode: 'aws' or 'gcp'" + }, + "aws_account_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Account Id", + "description": "Agent Factory's AWS Account ID (AWS mode only)" + }, + "external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Id", + "description": "Auto-generated External ID (AWS mode only)" + }, + "gcp_sa_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sa Email", + "description": "GCP Service Account email (GCP mode only)" + }, + "gcp_sa_unique_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sa Unique Id", + "description": "GCP SA unique ID (GCP mode only)" + }, + "google_thumbprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Google Thumbprint", + "description": "SHA1 thumbprint (GCP mode only)" + }, + "suggested_role_name": { + "type": "string", + "title": "Suggested Role Name", + "description": "Suggested IAM role name", + "default": "AgentFactoryDiscoveryRole" + }, + "policy_arn": { + "type": "string", + "title": "Policy Arn", + "description": "AWS managed policy ARN to attach", + "default": "arn:aws:iam::aws:policy/ReadOnlyAccess" + } + }, + "type": "object", + "required": [ + "identity_mode" + ], + "title": "DiscoverySetupInfo", + "description": "Setup information for discovery (AWS or GCP server identity)." + }, + "DiscoveryStatus": { + "type": "string", + "enum": [ + "not_started", + "cloud_linked", + "target_selected", + "repo_linked", + "boundaries_configured", + "inventory_scanning", + "inventory_ready", + "scanning", + "scanned", + "facets_linked", + "mapping", + "mapped", + "dag_building", + "dag_ready", + "dag_discovering", + "completed", + "error" + ], + "title": "DiscoveryStatus", + "description": "Cloud Discovery workflow status (cloud-agnostic)." + }, + "DiscoveryTagKeyOperatorUpdate": { + "properties": { + "tag_key": { + "type": "string", + "title": "Tag Key", + "description": "The tag key (e.g., 'env', 'team')" + }, + "operator": { + "type": "string", + "enum": [ + "and", + "or" + ], + "title": "Operator", + "description": "'and' or 'or'" + } + }, + "type": "object", + "required": [ + "tag_key", + "operator" + ], + "title": "DiscoveryTagKeyOperatorUpdate", + "description": "Request to update a tag key's join operator." + }, + "DiscoveryTagMatchModeUpdate": { + "properties": { + "tag_match_mode": { + "type": "string", + "enum": [ + "any", + "all" + ], + "title": "Tag Match Mode", + "description": "'any' (match any tag - OR) or 'all' (match all tags - AND)" + } + }, + "type": "object", + "required": [ + "tag_match_mode" + ], + "title": "DiscoveryTagMatchModeUpdate", + "description": "Request to update tag boundary matching mode." + }, + "EdgeCreate": { + "properties": { + "source_entry_id": { + "type": "string", + "title": "Source Entry Id" + }, + "target_name": { + "type": "string", + "title": "Target Name" + }, + "edge_type": { + "type": "string", + "title": "Edge Type" + }, + "details": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Details" + }, + "inferred": { + "type": "boolean", + "title": "Inferred", + "default": false + } + }, + "type": "object", + "required": [ + "source_entry_id", + "target_name", + "edge_type" + ], + "title": "EdgeCreate", + "description": "Request to add an edge (connection)." + }, + "EntityUsage": { + "properties": { + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "entity_type": { + "type": "string", + "title": "Entity Type" + }, + "entity_id": { + "type": "string", + "title": "Entity Id" + }, + "recall_count": { + "type": "integer", + "title": "Recall Count", + "default": 0 + }, + "last_recalled_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Recalled At" + }, + "last_actor": { + "$ref": "#/components/schemas/UsageActor" + } + }, + "type": "object", + "required": [ + "organization_id", + "entity_type", + "entity_id" + ], + "title": "EntityUsage", + "description": "Read-side projection of one `entity_usage` row.\n\nMissing entities are returned as a zeroed instance (see\n`UsageStatsStore.get_usage_bulk`) so the calling page can render a\ncolumn without per-row null checks." + }, + "ExternalA2AAgent": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "base_url": { + "type": "string", + "title": "Base Url" + }, + "rpc_url": { + "type": "string", + "title": "Rpc Url" + }, + "agent_card_cache": { + "additionalProperties": true, + "type": "object", + "title": "Agent Card Cache" + }, + "skills": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Skills" + }, + "auth_type": { + "$ref": "#/components/schemas/ExternalAgentAuthType" + }, + "auth_config": { + "$ref": "#/components/schemas/ExternalAgentAuthConfig" + }, + "status": { + "$ref": "#/components/schemas/ExternalAgentStatus", + "default": "connected" + }, + "last_verified_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Verified At" + }, + "last_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Error" + }, + "created_by": { + "type": "string", + "title": "Created By" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "display_name", + "base_url", + "rpc_url", + "auth_type", + "auth_config", + "created_by", + "created_at", + "updated_at" + ], + "title": "ExternalA2AAgent", + "description": "An external A2A agent registered at the org level." + }, + "ExternalAgentAuthConfig": { + "properties": { + "header_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Header Name" + }, + "credential_id": { + "type": "string", + "title": "Credential Id" + } + }, + "type": "object", + "required": [ + "credential_id" + ], + "title": "ExternalAgentAuthConfig", + "description": "Auth configuration for an external agent." + }, + "ExternalAgentAuthType": { + "type": "string", + "enum": [ + "api_key", + "bearer" + ], + "title": "ExternalAgentAuthType", + "description": "Supported auth types for external A2A agents." + }, + "ExternalAgentCreate": { + "properties": { + "base_url": { + "type": "string", + "title": "Base Url" + }, + "auth_type": { + "$ref": "#/components/schemas/ExternalAgentAuthType" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header" + }, + "auth_value": { + "type": "string", + "title": "Auth Value" + } + }, + "type": "object", + "required": [ + "base_url", + "auth_type", + "auth_value" + ], + "title": "ExternalAgentCreate", + "description": "Request to register a new external A2A agent." + }, + "ExternalAgentDiscoverResponse": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "url": { + "type": "string", + "title": "Url" + }, + "skills": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Skills" + }, + "auth_required": { + "type": "boolean", + "title": "Auth Required" + }, + "auth_schemes": { + "additionalProperties": true, + "type": "object", + "title": "Auth Schemes" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "url", + "skills", + "auth_required" + ], + "title": "ExternalAgentDiscoverResponse", + "description": "Response from discovering an external A2A agent." + }, + "ExternalAgentRequest": { + "properties": { + "session_key": { + "type": "string", + "title": "Session Key", + "description": "Unique key for session continuity. Format varies by source: 'slack:{channel}:{thread}', 'incident:{incident_id}', 'api:{request_id}'" + }, + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "Custom agent ID to run" + }, + "organization_id": { + "type": "string", + "title": "Organization Id", + "description": "Organization owning the agent" + }, + "message": { + "type": "string", + "title": "Message", + "description": "User message/prompt to the agent" + }, + "source": { + "$ref": "#/components/schemas/TriggerSource", + "description": "What triggered this request" + }, + "user": { + "$ref": "#/components/schemas/TriggerUser", + "description": "User who triggered the agent" + }, + "context": { + "$ref": "#/components/schemas/TriggerContext", + "description": "Additional context from trigger source" + }, + "resume_session": { + "type": "boolean", + "title": "Resume Session", + "description": "Whether to resume existing session if found", + "default": true + }, + "timeout_seconds": { + "type": "integer", + "title": "Timeout Seconds", + "description": "Max execution time in seconds", + "default": 300 + }, + "auto_approve_tools": { + "type": "boolean", + "title": "Auto Approve Tools", + "description": "Auto-approve all tool uses (for background/automated tasks)", + "default": false + }, + "auth_user": { + "anyOf": [ + { + "$ref": "#/components/schemas/AuthUser" + }, + { + "type": "null" + } + ], + "description": "AuthUser for agent tools that require user identity" + }, + "facets_context": { + "anyOf": [ + { + "$ref": "#/components/schemas/FacetsContext" + }, + { + "type": "null" + } + ], + "description": "FacetsContext for Facets operations (PAT, environment, etc.)" + }, + "files": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Files", + "description": "File attachments [{data: base64, media_type: str, name: str}]" + }, + "file_text_context": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Text Context", + "description": "Inline text content from code/text file attachments" + }, + "usage_user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Usage User Id", + "description": "Billing entity for usage tracking (who/what is spending). Slack: bot name, Migration: project name, Incident: incident ID. Falls back to agent_id if not set." + }, + "usage_interaction_id": { + "type": "string", + "title": "Usage Interaction Id", + "description": "Interaction identifier for usage tracking. Override for paths with meaningful per-interaction breakdown. When left as 'default', the runner bills against the session_key instead \u2014 SDK cost deltas are tracked per interaction doc, so a shared literal id would cross-contaminate sessions.", + "default": "default" + }, + "triggered_at": { + "type": "string", + "format": "date-time", + "title": "Triggered At" + } + }, + "type": "object", + "required": [ + "session_key", + "agent_id", + "organization_id", + "message", + "source", + "user" + ], + "title": "ExternalAgentRequest", + "description": "Request to run an agent from an external trigger.\n\nThis is the unified interface that all trigger sources convert to.\nThe ExternalAgentRunner accepts this and returns ExternalAgentResponse." + }, + "ExternalAgentResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether execution completed successfully" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "Error message if failed" + }, + "cancelled": { + "type": "boolean", + "title": "Cancelled", + "description": "Whether execution was cancelled by user", + "default": false + }, + "text": { + "type": "string", + "title": "Text", + "description": "Agent's text response (may be empty on error)", + "default": "" + }, + "output_data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Output Data", + "description": "Structured data from agent if available" + }, + "session_key": { + "type": "string", + "title": "Session Key", + "description": "Session key for future requests" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "Internal session ID for debugging" + }, + "execution_time_seconds": { + "type": "number", + "title": "Execution Time Seconds", + "description": "How long the agent ran", + "default": 0.0 + }, + "token_usage": { + "anyOf": [ + { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Token Usage", + "description": "Token usage breakdown" + }, + "cost_usd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost Usd", + "description": "Estimated cost in USD" + }, + "requires_followup": { + "type": "boolean", + "title": "Requires Followup", + "description": "Whether agent expects follow-up (e.g., waiting for user input)", + "default": false + }, + "followup_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Followup Prompt", + "description": "Suggested prompt for follow-up if needed" + }, + "suspended": { + "type": "boolean", + "title": "Suspended", + "description": "Whether execution is suspended awaiting user input (async question)", + "default": false + }, + "pending_question": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Pending Question", + "description": "Pending question details if suspended (question_id, text, options)" + } + }, + "type": "object", + "required": [ + "success", + "session_key" + ], + "title": "ExternalAgentResponse", + "description": "Response from running an agent via external trigger.\n\nContains the agent's output and metadata for the trigger source\nto format and deliver appropriately." + }, + "ExternalAgentResumeRequest": { + "properties": { + "session_key": { + "type": "string", + "title": "Session Key", + "description": "Session key to resume" + }, + "question_id": { + "type": "string", + "title": "Question Id", + "description": "ID of the question being answered" + }, + "answer": { + "type": "string", + "title": "Answer", + "description": "User's answer" + }, + "request": { + "$ref": "#/components/schemas/ExternalAgentRequest", + "description": "Original request (auth_user, facets_context, routing)" + } + }, + "type": "object", + "required": [ + "session_key", + "question_id", + "answer", + "request" + ], + "title": "ExternalAgentResumeRequest", + "description": "Wrapper for resuming a suspended run on a worker pod.\n\nresume_with_answer()'s signature isn't a single model, so this bundles its\narguments for the orchestrator->worker forward (POST /internal/\nexternal-agent-resume). ``request`` is the original ExternalAgentRequest,\nused both for agent context and to route to the same worker pod." + }, + "ExternalAgentStatus": { + "type": "string", + "enum": [ + "connected", + "error", + "auth_failed" + ], + "title": "ExternalAgentStatus", + "description": "Connection status of an external A2A agent." + }, + "FacetsConfig": { + "properties": { + "integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Id", + "description": "Facets integration ID (org-level)" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name", + "description": "Facets integration name for display" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "Facets project/stack ID" + }, + "project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Name", + "description": "Facets project name" + }, + "environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Id", + "description": "Facets environment/cluster ID" + }, + "environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Name", + "description": "Facets environment name" + }, + "project_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Type", + "description": "Project type (e.g., aws-k8s-standard)" + } + }, + "type": "object", + "title": "FacetsConfig", + "description": "Facets project and environment configuration." + }, + "FacetsContext": { + "properties": { + "facets_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Url" + }, + "facets_cookie": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Cookie" + }, + "facets_username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Username" + }, + "facets_pat": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Pat" + }, + "project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Name" + }, + "environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Id" + }, + "working_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Working Branch" + }, + "request_hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Request Hostname" + } + }, + "type": "object", + "title": "FacetsContext", + "description": "Facets-specific context information.\nSupports both cookie-based and basic authentication." + }, + "FacetsEnvironmentConfigResponse": { + "properties": { + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "facets_integration_id": { + "type": "string", + "title": "Facets Integration Id" + }, + "facets_integration_name": { + "type": "string", + "title": "Facets Integration Name" + }, + "facets_cp_url": { + "type": "string", + "title": "Facets Cp Url" + }, + "facets_project": { + "type": "string", + "title": "Facets Project" + }, + "facets_environment_id": { + "type": "string", + "title": "Facets Environment Id" + }, + "facets_environment_name": { + "type": "string", + "title": "Facets Environment Name" + }, + "has_k8s_cluster": { + "type": "boolean", + "title": "Has K8S Cluster", + "default": false + }, + "k8s_cluster_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Cluster Name" + } + }, + "type": "object", + "required": [ + "workspace_id", + "facets_integration_id", + "facets_integration_name", + "facets_cp_url", + "facets_project", + "facets_environment_id", + "facets_environment_name" + ], + "title": "FacetsEnvironmentConfigResponse", + "description": "Response for workspace Facets environment configuration." + }, + "FacetsEnvironmentConfigUpdate": { + "properties": { + "facets_integration_id": { + "type": "string", + "title": "Facets Integration Id", + "description": "Facets integration ID to use" + }, + "facets_project": { + "type": "string", + "title": "Facets Project", + "description": "Facets project/stack name" + }, + "facets_environment_id": { + "type": "string", + "title": "Facets Environment Id", + "description": "Facets cluster ID" + }, + "facets_environment_name": { + "type": "string", + "title": "Facets Environment Name", + "description": "Environment name" + }, + "has_k8s_cluster": { + "type": "boolean", + "title": "Has K8S Cluster", + "description": "Whether environment has K8s", + "default": false + }, + "k8s_cluster_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Cluster Name", + "description": "K8s cluster name" + } + }, + "type": "object", + "required": [ + "facets_integration_id", + "facets_project", + "facets_environment_id", + "facets_environment_name" + ], + "title": "FacetsEnvironmentConfigUpdate", + "description": "Schema for configuring Facets environment for a workspace." + }, + "FacetsEnvironmentInfo": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Environment/cluster ID from Facets CP" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Environment name (e.g., staging, prod)" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status", + "description": "Environment status (ACTIVE, etc)" + }, + "has_k8s": { + "type": "boolean", + "title": "Has K8S", + "description": "Whether K8s cluster is available", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "FacetsEnvironmentInfo", + "description": "Environment info returned from Facets CP.\n\nUsed for listing available environments in project." + }, + "FacetsEnvironmentListItem": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Environment/Cluster ID" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Environment name" + }, + "cluster_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cluster Code", + "description": "Cluster short code" + }, + "cloud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cloud", + "description": "Cloud provider" + }, + "state": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "State", + "description": "Cluster state" + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "FacetsEnvironmentListItem", + "description": "Facets environment/cluster for list view." + }, + "FacetsEnvironmentsListResponse": { + "properties": { + "environments": { + "items": { + "$ref": "#/components/schemas/FacetsEnvironmentListItem" + }, + "type": "array", + "title": "Environments" + }, + "project_id": { + "type": "string", + "title": "Project Id" + }, + "project_name": { + "type": "string", + "title": "Project Name" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "environments", + "project_id", + "project_name", + "total" + ], + "title": "FacetsEnvironmentsListResponse", + "description": "Response for listing Facets environments." + }, + "FacetsIntegrationCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-given name for this integration" + }, + "url": { + "type": "string", + "title": "Url", + "description": "Facets Control Plane URL (e.g., https://prod.facets.cloud)" + }, + "username": { + "type": "string", + "title": "Username", + "description": "Username (email) for Facets CP authentication" + }, + "pat": { + "type": "string", + "title": "Pat", + "description": "Personal Access Token (will be encrypted)" + } + }, + "type": "object", + "required": [ + "name", + "url", + "username", + "pat" + ], + "title": "FacetsIntegrationCreate", + "description": "Schema for creating/connecting a Facets integration." + }, + "FacetsIntegrationListResponse": { + "properties": { + "integrations": { + "items": { + "$ref": "#/components/schemas/FacetsIntegrationResponse" + }, + "type": "array", + "title": "Integrations" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "integrations", + "total" + ], + "title": "FacetsIntegrationListResponse", + "description": "API response for listing Facets integrations." + }, + "FacetsIntegrationResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "username": { + "type": "string", + "title": "Username" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "usage_count": { + "type": "integer", + "title": "Usage Count", + "description": "Number of workspaces using this integration", + "default": 0 + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "url", + "username", + "is_active", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "FacetsIntegrationResponse", + "description": "API response for Facets integration." + }, + "FacetsIntegrationUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "New name for the integration" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active", + "description": "Enable/disable integration" + } + }, + "type": "object", + "title": "FacetsIntegrationUpdate", + "description": "Schema for updating a Facets integration." + }, + "FacetsLinkRequest": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "facets_project_id": { + "type": "string", + "title": "Facets Project Id" + }, + "facets_project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Name" + }, + "facets_environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Id" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name" + }, + "facets_project_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Type" + } + }, + "type": "object", + "required": [ + "integration_id", + "facets_project_id" + ], + "title": "FacetsLinkRequest" + }, + "FacetsProjectInfo": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Project/Stack ID from Facets CP" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Project name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Project description" + }, + "project_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Type", + "description": "Project type (e.g., aws-eks-standard)" + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "FacetsProjectInfo", + "description": "Project/Stack info returned from Facets CP.\n\nUsed for listing available projects in an integration." + }, + "FacetsProjectListItem": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Project/Stack ID" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Project name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Project description" + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "FacetsProjectListItem", + "description": "Facets project/stack for list view." + }, + "FileContentRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "file_path": { + "type": "string", + "title": "File Path" + } + }, + "type": "object", + "required": [ + "session_id", + "file_path" + ], + "title": "FileContentRequest", + "description": "Request to read file content" + }, + "FileContentResponse": { + "properties": { + "content": { + "type": "string", + "title": "Content" + }, + "path": { + "type": "string", + "title": "Path" + }, + "name": { + "type": "string", + "title": "Name" + }, + "extension": { + "type": "string", + "title": "Extension" + }, + "size": { + "type": "integer", + "title": "Size" + }, + "modified": { + "type": "number", + "title": "Modified" + }, + "editable": { + "type": "boolean", + "title": "Editable", + "default": true + }, + "git_original": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Original" + }, + "git_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Status" + }, + "is_git_repo": { + "type": "boolean", + "title": "Is Git Repo", + "default": false + }, + "is_binary": { + "type": "boolean", + "title": "Is Binary", + "default": false + } + }, + "type": "object", + "required": [ + "content", + "path", + "name", + "extension", + "size", + "modified" + ], + "title": "FileContentResponse", + "description": "Response with file content" + }, + "FileNode": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "path": { + "type": "string", + "title": "Path" + }, + "type": { + "type": "string", + "title": "Type" + }, + "size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Size" + }, + "modified": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Modified" + }, + "children": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/FileNode" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Children" + }, + "extension": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Extension" + }, + "git_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Status" + }, + "git_root": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Root" + }, + "git_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Branch" + } + }, + "type": "object", + "required": [ + "name", + "path", + "type" + ], + "title": "FileNode", + "description": "Represents a file or directory in the tree" + }, + "FileSaveRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "file_path": { + "type": "string", + "title": "File Path" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "type": "object", + "required": [ + "session_id", + "file_path", + "content" + ], + "title": "FileSaveRequest", + "description": "Request to save file content" + }, + "FileTreeResponse": { + "properties": { + "root_path": { + "type": "string", + "title": "Root Path" + }, + "tree": { + "items": { + "$ref": "#/components/schemas/FileNode" + }, + "type": "array", + "title": "Tree" + }, + "session_id": { + "type": "string", + "title": "Session Id" + }, + "is_git_repo": { + "type": "boolean", + "title": "Is Git Repo", + "default": false + }, + "git_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Branch" + } + }, + "type": "object", + "required": [ + "root_path", + "tree", + "session_id" + ], + "title": "FileTreeResponse", + "description": "Response with file tree structure" + }, + "FileUploadResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "file_path": { + "type": "string", + "title": "File Path" + }, + "file_name": { + "type": "string", + "title": "File Name" + }, + "file_size": { + "type": "integer", + "title": "File Size" + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "success", + "file_path", + "file_name", + "file_size", + "message" + ], + "title": "FileUploadResponse", + "description": "Response model for file upload." + }, + "FilterRule": { + "properties": { + "op": { + "type": "string", + "enum": [ + "contains", + "not_contains" + ], + "title": "Op" + }, + "value": { + "type": "string", + "minLength": 1, + "title": "Value", + "description": "Filter value \u2014 must be non-empty." + } + }, + "type": "object", + "required": [ + "op", + "value" + ], + "title": "FilterRule", + "description": "A single filter rule with an operator and a value." + }, + "Finding": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Finding title" + }, + "severity": { + "$ref": "#/components/schemas/FindingSeverity", + "description": "Finding severity" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Detailed finding description" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment", + "description": "Environment where finding was discovered" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Service", + "description": "Service related to the finding" + }, + "raw_data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Raw Data", + "description": "Raw data backing the finding" + }, + "artifact_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Artifact Id", + "description": "Linked artifact for full deliverable" + }, + "finding_key": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ], + "title": "Finding Key", + "description": "Stable identity slug. Caller-supplied via record_finding or auto-derived as sha1(title|service|environment)[:16]. Two findings with the same (schedule_id, finding_key) are treated as the same recurring issue." + }, + "first_seen_run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "First Seen Run Id", + "description": "Run id of the FIRST run where this finding_key surfaced." + }, + "last_seen_run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Seen Run Id", + "description": "Run id of the MOST RECENT run where this finding_key surfaced. Equal to the containing run's id when first written; updated by push_finding on subsequent runs." + }, + "recurrence_count": { + "type": "integer", + "minimum": 1.0, + "title": "Recurrence Count", + "description": "Number of consecutive runs this finding_key has been open.", + "default": 1 + }, + "status": { + "$ref": "#/components/schemas/FindingStatus", + "description": "open | resolved | suppressed", + "default": "open" + }, + "resolved_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Resolved At", + "description": "When status flipped to 'resolved'. None while open." + }, + "last_seen_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Seen At", + "description": "started_at of the most recent run where this finding_key surfaced. Populated by list_findings_for_schedule only \u2014 absent on per-run reads." + } + }, + "type": "object", + "required": [ + "title", + "severity", + "description" + ], + "title": "Finding", + "description": "A finding discovered during a scheduled agent run." + }, + "FindingSeverity": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low", + "info" + ], + "title": "FindingSeverity", + "description": "Severity level for findings" + }, + "FindingStatus": { + "type": "string", + "enum": [ + "open", + "resolved", + "suppressed" + ], + "title": "FindingStatus", + "description": "Lifecycle status of a recurring finding.\n\nopen \u2014 currently observed; will be re-checked on next run\nresolved \u2014 auto-resolved after K clean runs OR explicitly closed\nsuppressed \u2014 user marked \"ignore\"; reserved for Phase 4 UI" + }, + "FromChatRequest": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id", + "description": "Source chat session ID" + }, + "name": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Name" + }, + "display_name": { + "type": "string", + "maxLength": 100, + "title": "Display Name" + }, + "cron_expression": { + "type": "string", + "title": "Cron Expression" + }, + "timezone": { + "type": "string", + "title": "Timezone", + "default": "UTC" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "objective": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ], + "title": "Objective" + }, + "objective_seed": { + "anyOf": [ + { + "type": "string", + "maxLength": 20000 + }, + { + "type": "null" + } + ], + "title": "Objective Seed", + "description": "When provided, used verbatim as the objective. Otherwise the handler derives a short objective from the chat transcript." + }, + "targets": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ScheduleTarget" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Targets" + }, + "slack_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/SlackConfig" + }, + { + "type": "null" + } + ] + }, + "git_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/GitConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "session_id", + "name", + "display_name", + "cron_expression" + ], + "title": "FromChatRequest", + "description": "Body for POST /schedules/from_chat (Phase 4)." + }, + "GcpCredentials": { + "properties": { + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "GCP provider", + "default": "gcp" + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regions", + "description": "Regions to scan" + }, + "validated": { + "type": "boolean", + "title": "Validated", + "description": "Whether credentials validated", + "default": false + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error", + "description": "Error if validation failed" + }, + "service_account_email": { + "type": "string", + "title": "Service Account Email", + "description": "Service Account email" + }, + "project_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Project Ids", + "description": "GCP Project IDs to scan" + } + }, + "type": "object", + "required": [ + "service_account_email" + ], + "title": "GcpCredentials", + "description": "GCP credentials for cross-project access via Service Account." + }, + "GenericWebhookCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "GenericWebhookCreate", + "description": "Request body for creating a webhook." + }, + "GenericWebhookFilterUpdate": { + "properties": { + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + } + }, + "type": "object", + "title": "GenericWebhookFilterUpdate", + "description": "Request body for updating webhook filter rules." + }, + "GenericWebhookResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "webhook_id": { + "type": "string", + "title": "Webhook Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "webhook_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Webhook Url" + } + }, + "type": "object", + "required": [ + "id", + "webhook_id", + "organization_id", + "workspace_id", + "name", + "is_active", + "created_at", + "updated_at" + ], + "title": "GenericWebhookResponse", + "description": "API response for a webhook configuration." + }, + "GitConfig": { + "properties": { + "repository_id": { + "type": "string", + "title": "Repository Id", + "description": "Link to repositories collection" + }, + "auth_mode": { + "type": "string", + "title": "Auth Mode", + "description": "Authentication mode: 'pat' or 'github_app_installation'", + "default": "pat" + }, + "pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Id", + "description": "PAT credential ID (required when auth_mode='pat')" + }, + "default_branch": { + "type": "string", + "title": "Default Branch", + "description": "Default branch name", + "default": "main" + } + }, + "type": "object", + "required": [ + "repository_id" + ], + "title": "GitConfig", + "description": "Git repository configuration.\n\nAuth mode determines how the schedule authenticates:\n- 'pat': Uses a stored PAT (decrypted at execution time)\n- 'github_app_installation': Uses the org's GitHub App installation token (generated per run)" + }, + "GitHubAppInstallUrlResponse": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "webhook_id": { + "type": "string", + "title": "Webhook Id" + } + }, + "type": "object", + "required": [ + "url", + "webhook_id" + ], + "title": "GitHubAppInstallUrlResponse", + "description": "Response from POST /integrations/github-app/install-url." + }, + "GitHubAppInstallationResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "installation_id": { + "type": "integer", + "title": "Installation Id" + }, + "account_login": { + "type": "string", + "title": "Account Login" + }, + "account_type": { + "type": "string", + "title": "Account Type" + }, + "repository_selection": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Repository Selection" + }, + "installed_by_email": { + "type": "string", + "title": "Installed By Email" + }, + "status": { + "$ref": "#/components/schemas/GitHubAppInstallationStatus" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "installation_id", + "account_login", + "account_type", + "installed_by_email", + "status", + "created_at", + "updated_at" + ], + "title": "GitHubAppInstallationResponse", + "description": "API response for a GitHub App installation (without MongoDB alias)." + }, + "GitHubAppInstallationStatus": { + "type": "string", + "enum": [ + "active", + "suspended", + "deleted" + ], + "title": "GitHubAppInstallationStatus", + "description": "Status of a GitHub App installation." + }, + "GitHubAppStatusResponse": { + "properties": { + "installed": { + "type": "boolean", + "title": "Installed" + }, + "installation": { + "anyOf": [ + { + "$ref": "#/components/schemas/GitHubAppInstallationResponse" + }, + { + "type": "null" + } + ] + }, + "settings_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Settings Url" + } + }, + "type": "object", + "required": [ + "installed" + ], + "title": "GitHubAppStatusResponse", + "description": "Response from GET /integrations/github-app/status." + }, + "GitPATCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-friendly name for the PAT" + }, + "provider": { + "$ref": "#/components/schemas/GitProvider", + "description": "Git provider" + }, + "token": { + "type": "string", + "title": "Token", + "description": "Personal access token (will be encrypted)" + }, + "git_email": { + "type": "string", + "title": "Git Email", + "description": "Email address associated with the Git account" + }, + "enterprise_hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Enterprise Hostname", + "description": "GitHub Enterprise hostname (e.g., 'github.mycompany.com'). Only for GitHub provider." + } + }, + "type": "object", + "required": [ + "name", + "provider", + "token", + "git_email" + ], + "title": "GitPATCreate", + "description": "Schema for creating a new Git PAT" + }, + "GitPATListResponse": { + "properties": { + "pats": { + "items": { + "$ref": "#/components/schemas/GitPATResponse" + }, + "type": "array", + "title": "Pats" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "pats", + "total" + ], + "title": "GitPATListResponse", + "description": "Response model for listing Git PATs" + }, + "GitPATResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "provider": { + "$ref": "#/components/schemas/GitProvider" + }, + "email": { + "type": "string", + "title": "Email" + }, + "owner_email": { + "type": "string", + "title": "Owner Email" + }, + "organization_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Organization Id" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_used_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Used At" + }, + "token_type": { + "$ref": "#/components/schemas/TokenType", + "default": "pat" + }, + "oauth_scopes": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Oauth Scopes" + }, + "oauth_expires_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Oauth Expires At" + }, + "github_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Github Id" + }, + "enterprise_hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Enterprise Hostname" + } + }, + "type": "object", + "required": [ + "id", + "name", + "provider", + "email", + "owner_email", + "created_at", + "updated_at" + ], + "title": "GitPATResponse", + "description": "Schema for Git PAT API responses (without exposing token)" + }, + "GitPATUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "New name for the PAT" + }, + "token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Token", + "description": "New token (will be encrypted)" + } + }, + "type": "object", + "title": "GitPATUpdate", + "description": "Schema for updating an existing Git PAT" + }, + "GitProvider": { + "type": "string", + "enum": [ + "github", + "gitlab", + "bitbucket" + ], + "title": "GitProvider", + "description": "Supported Git providers" + }, + "GroupedSessions": { + "properties": { + "today": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Today" + }, + "yesterday": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Yesterday" + }, + "previous_7_days": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Previous 7 Days" + }, + "previous_30_days": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Previous 30 Days" + }, + "older": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Older" + } + }, + "type": "object", + "required": [ + "today", + "yesterday", + "previous_7_days", + "previous_30_days", + "older" + ], + "title": "GroupedSessions", + "description": "Sessions grouped by date ranges." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HistoryAction": { + "type": "string", + "enum": [ + "created", + "uploaded", + "analyzed", + "edited", + "correction", + "exported", + "item_added", + "item_updated", + "item_deleted", + "file_uploaded", + "analysis_started", + "analysis_failed", + "price_lookup" + ], + "title": "HistoryAction", + "description": "History entry action types." + }, + "IgCatalogSummary": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "version": { + "type": "string", + "title": "Version" + }, + "built_at": { + "type": "string", + "title": "Built At" + }, + "members": { + "items": { + "$ref": "#/components/schemas/IgMemberMeta" + }, + "type": "array", + "title": "Members" + } + }, + "type": "object", + "required": [ + "name", + "version", + "built_at" + ], + "title": "IgCatalogSummary", + "description": "The catalog manifest \u2014 ``ig``'s ``metadata.json`` \u2014 without member blobs.\n\nThe real ``ig`` binary writes the catalog name under the key ``catalog``\n(alongside ``schema_version`` / ``ig_version`` / ``routes``, which we ignore).\nOlder/synthesized metadata and internal callers use ``name``. We accept\neither via a validation alias so ``IgCatalogSummary(**metadata_json)`` parses\nig's output verbatim, while ``populate_by_name`` keeps ``name=`` construction\nworking. Serialization always emits ``name`` (no serialization alias is set),\nwhich the HTTP API and the CLI depend on." + }, + "IgClaimsResponse": { + "properties": { + "git": { + "type": "string", + "title": "Git" + }, + "catalogs": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Catalogs" + } + }, + "type": "object", + "required": [ + "git" + ], + "title": "IgClaimsResponse", + "description": "Reverse lookup: which catalogs claim a given canonical git URL.\n\nBacks ``praxis ig claims --git `` \u2014 the query repo CI runs to discover\nwhich catalogs embed it." + }, + "IgInfraRefreshRequest": { + "properties": { + "project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project" + }, + "facets_project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project" + } + }, + "type": "object", + "title": "IgInfraRefreshRequest", + "description": "Body for ``POST /ig/infra/refresh`` \u2014 the project-keyed infra refresh.\n\nA Facets deploy webhook (a WEBHOOK notification channel subscribed to\n``APP_DEPLOYMENT``) POSTs a JSON body rendered from a Mustache template the\noperator authors. Since the template author picks the field name, we accept\neither ``project`` or ``facets_project``; exactly one must be present and\nnon-empty (both, or neither, is a 422). :attr:`resolved_project` is the\ntrimmed value." + }, + "IgManifest": { + "properties": { + "catalog": { + "type": "string", + "title": "Catalog" + }, + "content": { + "type": "string", + "title": "Content" + }, + "pushed_by": { + "type": "string", + "title": "Pushed By" + }, + "pushed_at": { + "type": "string", + "format": "date-time", + "title": "Pushed At" + }, + "git_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Sha" + } + }, + "type": "object", + "required": [ + "catalog", + "content", + "pushed_by", + "pushed_at" + ], + "title": "IgManifest", + "description": "A catalog's ``ig`` manifest \u2014 the small YAML text authored on a laptop.\n\n``ig`` never owns the manifest's content; the server only stores the text\nverbatim plus provenance stamps. ``pushed_by`` is the caller's email,\n``pushed_at`` the server-side push time, and ``git_sha`` the client-supplied\ncommit the manifest came from. Push-only: there is no server-side editing." + }, + "IgManifestPushRequest": { + "properties": { + "content": { + "type": "string", + "title": "Content" + }, + "git_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git Sha" + } + }, + "type": "object", + "required": [ + "content" + ], + "title": "IgManifestPushRequest", + "description": "Body for ``POST /catalogs/{catalog}/manifest`` (``praxis ig manifest push``)." + }, + "IgMemberBlobRef": { + "properties": { + "catalog": { + "type": "string", + "title": "Catalog" + }, + "member": { + "type": "string", + "title": "Member" + }, + "git": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git" + }, + "sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sha" + }, + "blob_id": { + "type": "string", + "title": "Blob Id" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes" + }, + "digest": { + "type": "string", + "title": "Digest" + } + }, + "type": "object", + "required": [ + "catalog", + "member", + "blob_id", + "size_bytes", + "digest" + ], + "title": "IgMemberBlobRef", + "description": "A pointer to one stored member blob (gzipped ``graph.json`` in GridFS).\n\nReturned by ``IgCatalogStore.put_member``. ``blob_id`` is the stringified\nGridFS file id; ``digest`` is the ``sha256`` hex of the gzipped bytes." + }, + "IgMemberMeta": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "kind": { + "type": "string", + "enum": [ + "code", + "infra" + ], + "title": "Kind" + }, + "git": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Git" + }, + "sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sha" + } + }, + "type": "object", + "required": [ + "name", + "kind" + ], + "title": "IgMemberMeta", + "description": "One member of a catalog, as recorded in ``ig``'s ``member-meta.json``.\n\n``git`` / ``sha`` are the canonical identity of a ``code`` member. ``infra``\nmembers (the project's Facets graph) have no repo, so both are ``None``." + }, + "ImportConflictInfo": { + "properties": { + "has_conflict": { + "type": "boolean", + "title": "Has Conflict", + "default": false + }, + "existing_agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Existing Agent Id" + }, + "existing_agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Existing Agent Name" + }, + "suggested_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Suggested Name" + } + }, + "type": "object", + "title": "ImportConflictInfo", + "description": "Information about name conflicts during import" + }, + "ImportPreviewResponse": { + "properties": { + "valid": { + "type": "boolean", + "title": "Valid" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "agent_display_name": { + "type": "string", + "title": "Agent Display Name" + }, + "model": { + "$ref": "#/components/schemas/AgentModel" + }, + "conflict": { + "$ref": "#/components/schemas/ImportConflictInfo" + }, + "mcp_count": { + "type": "integer", + "title": "Mcp Count", + "default": 0 + }, + "has_repository": { + "type": "boolean", + "title": "Has Repository", + "default": false + }, + "repository_info": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepositoryImportInfo" + }, + { + "type": "null" + } + ] + }, + "credentials_required": { + "items": { + "$ref": "#/components/schemas/CredentialReference" + }, + "type": "array", + "title": "Credentials Required" + }, + "validation_errors": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Validation Errors" + } + }, + "type": "object", + "required": [ + "valid", + "agent_name", + "agent_display_name", + "model", + "conflict" + ], + "title": "ImportPreviewResponse", + "description": "Preview response when importing an agent" + }, + "ImportRequest": { + "properties": { + "data": { + "$ref": "#/components/schemas/AgentExportPackage" + }, + "conflict_resolution": { + "anyOf": [ + { + "type": "string", + "enum": [ + "rename", + "overwrite" + ] + }, + { + "type": "null" + } + ], + "title": "Conflict Resolution" + }, + "link_repository_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Link Repository Id" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "ImportRequest", + "description": "Request to import an agent" + }, + "ImportResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "created_mcps": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Created Mcps" + }, + "created_repository": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created Repository" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Warnings" + } + }, + "type": "object", + "required": [ + "success", + "agent_id", + "agent_name" + ], + "title": "ImportResponse", + "description": "Response after successfully importing an agent" + }, + "InboundConnection": { + "properties": { + "source_repo_id": { + "type": "string", + "title": "Source Repo Id" + }, + "source_repo_name": { + "type": "string", + "title": "Source Repo Name" + }, + "connection_type": { + "type": "string", + "title": "Connection Type" + }, + "evidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence" + }, + "confidence": { + "type": "number", + "title": "Confidence" + } + }, + "type": "object", + "required": [ + "source_repo_id", + "source_repo_name", + "connection_type", + "evidence", + "confidence" + ], + "title": "InboundConnection", + "description": "An edge pointing at the requested repo from another repo." + }, + "IncidentChannelConfigCreate": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "Chat integration ID to use" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Channel ID to post incidents" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel name for display" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + } + }, + "type": "object", + "required": [ + "integration_id", + "channel_id", + "channel_name" + ], + "title": "IncidentChannelConfigCreate", + "description": "Schema for creating/updating incident channel configuration." + }, + "IncidentCreate": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "Incident title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Incident description" + }, + "service": { + "type": "string", + "title": "Service", + "description": "Affected service name" + }, + "severity": { + "$ref": "#/components/schemas/entities__incident__incident_models__Severity", + "description": "Severity level", + "default": "P3" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment", + "description": "Environment" + }, + "source": { + "$ref": "#/components/schemas/AlertSource", + "description": "Alert source", + "default": "manual" + }, + "external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Id", + "description": "External ID" + }, + "alert_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Alert Url", + "description": "Link to source alert" + } + }, + "type": "object", + "required": [ + "title", + "service" + ], + "title": "IncidentCreate", + "description": "Schema for creating an incident." + }, + "IncidentDashboardStats": { + "properties": { + "total_incidents": { + "type": "integer", + "title": "Total Incidents" + }, + "open_incidents": { + "type": "integer", + "title": "Open Incidents" + }, + "critical_open": { + "type": "integer", + "title": "Critical Open" + }, + "mtta_minutes": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Mtta Minutes" + }, + "mtti_minutes": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Mtti Minutes" + }, + "by_status": { + "items": { + "$ref": "#/components/schemas/StatusCount" + }, + "type": "array", + "title": "By Status" + }, + "by_severity": { + "items": { + "$ref": "#/components/schemas/SeverityCount" + }, + "type": "array", + "title": "By Severity" + }, + "by_root_cause": { + "items": { + "$ref": "#/components/schemas/RootCauseCount" + }, + "type": "array", + "title": "By Root Cause" + }, + "top_services": { + "items": { + "$ref": "#/components/schemas/ServiceCount" + }, + "type": "array", + "title": "Top Services" + }, + "daily_trend": { + "items": { + "$ref": "#/components/schemas/DailyTrend" + }, + "type": "array", + "title": "Daily Trend" + }, + "investigations_completed": { + "type": "integer", + "title": "Investigations Completed" + }, + "investigations_failed": { + "type": "integer", + "title": "Investigations Failed" + }, + "investigation_success_rate": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Investigation Success Rate" + }, + "total_cost": { + "type": "number", + "title": "Total Cost" + }, + "avg_cost_per_incident": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Cost Per Incident" + }, + "period_days": { + "type": "integer", + "title": "Period Days" + } + }, + "type": "object", + "required": [ + "total_incidents", + "open_incidents", + "critical_open", + "by_status", + "by_severity", + "by_root_cause", + "top_services", + "daily_trend", + "investigations_completed", + "investigations_failed", + "total_cost", + "period_days" + ], + "title": "IncidentDashboardStats", + "description": "Dashboard statistics response." + }, + "IncidentListResponse": { + "properties": { + "incidents": { + "items": { + "$ref": "#/components/schemas/IncidentResponse" + }, + "type": "array", + "title": "Incidents" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "incidents", + "total" + ], + "title": "IncidentListResponse", + "description": "API response for incident list." + }, + "IncidentResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "service": { + "type": "string", + "title": "Service" + }, + "severity": { + "$ref": "#/components/schemas/Severity-Output" + }, + "environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment" + }, + "source": { + "$ref": "#/components/schemas/AlertSource" + }, + "external_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Id" + }, + "alert_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Alert Url" + }, + "status": { + "$ref": "#/components/schemas/IncidentStatus" + }, + "investigation_status": { + "$ref": "#/components/schemas/InvestigationStatus", + "default": "not_started" + }, + "investigation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Investigation Error" + }, + "triggered_at": { + "type": "string", + "format": "date-time", + "title": "Triggered At" + }, + "acknowledged_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Acknowledged At" + }, + "resolved_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Resolved At" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "investigation": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Investigation" + }, + "investigation_cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Investigation Cost" + }, + "triage_classification": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Triage Classification" + }, + "parent_incident_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Parent Incident Id" + }, + "triage_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Triage Reason" + }, + "occurrence_count": { + "type": "integer", + "title": "Occurrence Count", + "default": 0 + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "workspace_id", + "title", + "description", + "service", + "severity", + "environment", + "source", + "external_id", + "alert_url", + "status", + "triggered_at", + "acknowledged_at", + "resolved_at", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "IncidentResponse", + "description": "API response for an incident." + }, + "IncidentStatus": { + "type": "string", + "enum": [ + "triggered", + "acknowledged", + "resolved" + ], + "title": "IncidentStatus", + "description": "Incident lifecycle status (human workflow)." + }, + "IntegrationGate": { + "properties": { + "enabled_for_chat": { + "type": "boolean", + "title": "Enabled For Chat", + "description": "Usable from an interactive chat starter" + }, + "enabled_for_schedule": { + "type": "boolean", + "title": "Enabled For Schedule", + "description": "Usable from a scheduled (unattended) agent run" + }, + "schedule_blocker": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Schedule Blocker", + "description": "Human-readable reason scheduling is unavailable, if any" + } + }, + "type": "object", + "required": [ + "enabled_for_chat", + "enabled_for_schedule" + ], + "title": "IntegrationGate", + "description": "Per-integration availability split by consumer context.\n\nSome integrations (notably per-user PATs) are safe to use in an\ninteractive chat \u2014 the user is present to consent to the token \u2014\nbut unsafe in a scheduled/cron run where no human is in the loop.\nConsumers gate accordingly." + }, + "IntegrationsStatusResponse": { + "properties": { + "enabled": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Enabled", + "description": "Integration keys with at least one active connection" + }, + "disabled": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Disabled", + "description": "Shippable integration keys with no active connection" + }, + "unavailable": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Unavailable", + "description": "Integration keys not yet shippable in the product" + }, + "integrations": { + "additionalProperties": { + "$ref": "#/components/schemas/IntegrationGate" + }, + "type": "object", + "title": "Integrations", + "description": "Per-key availability split by consumer (chat vs schedule). Most integrations are uniformly available in both contexts; VCS PATs (gitlab, bitbucket, and the synthetic 'code-repo' key) differ because they're per-user credentials." + } + }, + "type": "object", + "title": "IntegrationsStatusResponse", + "description": "Per-org connection state for every integration the catalog knows." + }, + "InvestigationStatus": { + "type": "string", + "enum": [ + "not_started", + "in_progress", + "completed", + "failed", + "timeout", + "skipped" + ], + "title": "InvestigationStatus", + "description": "AI investigation status (separate from incident lifecycle)." + }, + "InvokeRequest": { + "properties": { + "extra_instructions": { + "anyOf": [ + { + "type": "string", + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Extra Instructions", + "description": "Optional one-shot instructions for this run (free text)" + } + }, + "type": "object", + "title": "InvokeRequest", + "description": "Request body for firing an agent webhook.\n\n`extra_instructions` is plain-English instructions for this run\nonly \u2014 appended to the webhook's stored objective. Empty body is\nfine; the webhook will run on the stored objective alone." + }, + "InvokeResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "run_id": { + "type": "string", + "title": "Run Id" + }, + "webhook_id": { + "type": "string", + "title": "Webhook Id" + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "success", + "run_id", + "webhook_id", + "message" + ], + "title": "InvokeResponse", + "description": "Response from firing an agent webhook.\n\nThe webhook runs asynchronously in the background. The caller gets\na `run_id` immediately and can poll for status / artifacts via the\nauthenticated management API." + }, + "K8sActivityPatch": { + "properties": { + "source_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Integration Id" + }, + "source_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Integration Name" + }, + "facets_project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Id" + }, + "facets_project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Name" + }, + "facets_environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Id" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name" + }, + "facets_project_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Type" + }, + "chat_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Chat Session Id" + } + }, + "type": "object", + "title": "K8sActivityPatch", + "description": "Partial update to K8s activity metadata.\n\nOnly fields the user is allowed to set from outside the discovery\nflow are exposed. Status transitions go through dedicated discovery\nendpoints, not this PATCH." + }, + "K8sActivityState": { + "properties": { + "status": { + "$ref": "#/components/schemas/K8sProjectStatus", + "default": "draft" + }, + "source_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Integration Id" + }, + "source_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Integration Name" + }, + "facets_project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Id" + }, + "facets_project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Name" + }, + "facets_environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Id" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name" + }, + "facets_project_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project Type" + }, + "chat_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Chat Session Id" + }, + "history": { + "items": { + "$ref": "#/components/schemas/entities__k8s_migration__k8s_migration_models__HistoryEntry" + }, + "type": "array", + "title": "History" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "title": "K8sActivityState", + "description": "Embedded K8s migration activity state on a parent MigrationProject.\n\nPresent on a MigrationProject iff the user has started the K8s\nMigration activity for that project. Identity (id, name, description,\norganization_id, created_by_email) lives on the parent.\n\nWorkload/Helm/wiring scan results stay in the\n`k8s_discovery_configs` collection, keyed by the parent\nMigrationProject.id." + }, + "K8sProjectStatus": { + "type": "string", + "enum": [ + "draft", + "scanning", + "discovered", + "generating", + "completed", + "error" + ], + "title": "K8sProjectStatus", + "description": "K8s migration project lifecycle status." + }, + "K8sSourceCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Name" + }, + "kubeconfig": { + "type": "string", + "title": "Kubeconfig", + "description": "Plain kubeconfig YAML (encrypted before storage)" + } + }, + "type": "object", + "required": [ + "name", + "kubeconfig" + ], + "title": "K8sSourceCreate", + "description": "Request body for creating a K8s source." + }, + "K8sSourceListResponse": { + "properties": { + "integrations": { + "items": { + "$ref": "#/components/schemas/K8sSourceResponse" + }, + "type": "array", + "title": "Integrations" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "integrations", + "total" + ], + "title": "K8sSourceListResponse", + "description": "Response for listing K8s sources." + }, + "K8sSourceResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "source_kind": { + "type": "string", + "enum": [ + "kubeconfig", + "gke", + "eks" + ], + "title": "Source Kind", + "default": "kubeconfig" + }, + "cloud_source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cloud Source Id" + }, + "cluster": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cluster" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Location" + }, + "cluster_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cluster Name" + }, + "context_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Context Name" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "validated": { + "type": "boolean", + "title": "Validated" + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "usage_count": { + "type": "integer", + "title": "Usage Count", + "default": 0 + } + }, + "type": "object", + "required": [ + "id", + "integration_id", + "organization_id", + "name", + "is_active", + "validated", + "created_at", + "updated_at" + ], + "title": "K8sSourceResponse", + "description": "API response for a K8s source - NEVER includes encrypted_kubeconfig." + }, + "K8sSourceUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string", + "maxLength": 200, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + }, + "kubeconfig": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Kubeconfig", + "description": "New kubeconfig YAML (will be re-encrypted)" + } + }, + "type": "object", + "title": "K8sSourceUpdate", + "description": "Request body for updating a K8s source." + }, + "KubernetesConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Auto-load kubeconfig", + "default": false + }, + "default_cluster_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Cluster Id", + "description": "Default cluster to connect" + }, + "auto_switch_context": { + "type": "boolean", + "title": "Auto Switch Context", + "description": "Auto-switch kubectl context", + "default": true + } + }, + "type": "object", + "title": "KubernetesConfig", + "description": "Kubernetes integration configuration" + }, + "KubernetesConfigExport": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + }, + "auto_switch_context": { + "type": "boolean", + "title": "Auto Switch Context", + "default": true + } + }, + "type": "object", + "title": "KubernetesConfigExport", + "description": "Kubernetes config for export (no IDs)" + }, + "LineItem": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "service_category": { + "type": "string", + "title": "Service Category", + "description": "Compute, Database, Storage, Network, Monitoring, Container, Messaging, Other", + "default": "Other" + }, + "aws_service": { + "type": "string", + "title": "Aws Service", + "description": "AWS service name (exact from CSV)" + }, + "aws_region": { + "type": "string", + "title": "Aws Region", + "description": "AWS region" + }, + "aws_sku": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Sku", + "description": "AWS SKU identifier" + }, + "aws_unit_price": { + "type": "number", + "title": "Aws Unit Price", + "description": "AWS unit price", + "default": 0.0 + }, + "aws_quantity": { + "type": "number", + "title": "Aws Quantity", + "description": "AWS quantity", + "default": 0.0 + }, + "aws_usage_unit": { + "type": "string", + "title": "Aws Usage Unit", + "description": "AWS usage unit (e.g., vCPU-Hours, GB-Hours, GB)", + "default": "" + }, + "aws_monthly_cost": { + "type": "number", + "title": "Aws Monthly Cost", + "description": "AWS monthly cost" + }, + "gcp_service": { + "type": "string", + "title": "Gcp Service", + "description": "GCP equivalent service", + "default": "" + }, + "gcp_region": { + "type": "string", + "title": "Gcp Region", + "description": "GCP target region", + "default": "" + }, + "gcp_sku_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id", + "description": "GCP SKU ID for on-demand" + }, + "gcp_sku_id_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id Cud", + "description": "GCP SKU ID for 1yr CUD" + }, + "gcp_sku_id_3yr_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id 3Yr Cud", + "description": "GCP SKU ID for 3yr CUD" + }, + "gcp_ondemand_unit_price": { + "type": "number", + "title": "Gcp Ondemand Unit Price", + "description": "GCP on-demand unit price", + "default": 0.0 + }, + "gcp_1yr_cud_unit_price": { + "type": "number", + "title": "Gcp 1Yr Cud Unit Price", + "description": "GCP 1-year CUD unit price", + "default": 0.0 + }, + "gcp_3yr_cud_unit_price": { + "type": "number", + "title": "Gcp 3Yr Cud Unit Price", + "description": "GCP 3-year CUD unit price", + "default": 0.0 + }, + "gcp_quantity": { + "type": "number", + "title": "Gcp Quantity", + "description": "GCP quantity (e.g., vCPUs, GB)", + "default": 0.0 + }, + "gcp_usage_unit": { + "type": "string", + "title": "Gcp Usage Unit", + "description": "Usage unit: hour, GB-month, etc.", + "default": "hour" + }, + "gcp_unit_price": { + "type": "number", + "title": "Gcp Unit Price", + "description": "DEPRECATED: use gcp_ondemand_unit_price", + "default": 0.0 + }, + "gcp_ondemand_cost": { + "type": "number", + "title": "Gcp Ondemand Cost", + "description": "GCP on-demand monthly cost (calculated)", + "default": 0.0 + }, + "gcp_1yr_cud_cost": { + "type": "number", + "title": "Gcp 1Yr Cud Cost", + "description": "GCP 1-year CUD monthly cost (calculated)", + "default": 0.0 + }, + "gcp_3yr_cud_cost": { + "type": "number", + "title": "Gcp 3Yr Cud Cost", + "description": "GCP 3-year CUD monthly cost (calculated)", + "default": 0.0 + }, + "is_gcp_advantage": { + "type": "boolean", + "title": "Is Gcp Advantage", + "description": "True if this is a free GCP feature", + "default": false + }, + "is_excluded": { + "type": "boolean", + "title": "Is Excluded", + "description": "True if user excluded this from totals", + "default": false + }, + "notes": { + "type": "string", + "title": "Notes", + "description": "User/agent notes", + "default": "" + } + }, + "additionalProperties": true, + "type": "object", + "required": [ + "aws_service", + "aws_region", + "aws_monthly_cost" + ], + "title": "LineItem", + "description": "Individual cost comparison line item." + }, + "LineItemCreate": { + "properties": { + "service_category": { + "type": "string", + "title": "Service Category", + "default": "Other" + }, + "aws_service": { + "type": "string", + "title": "Aws Service" + }, + "aws_region": { + "type": "string", + "title": "Aws Region" + }, + "aws_monthly_cost": { + "type": "number", + "title": "Aws Monthly Cost" + }, + "aws_sku": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Sku" + }, + "aws_unit_price": { + "type": "number", + "title": "Aws Unit Price", + "default": 0.0 + }, + "aws_quantity": { + "type": "number", + "title": "Aws Quantity", + "default": 0.0 + }, + "aws_usage_unit": { + "type": "string", + "title": "Aws Usage Unit", + "default": "" + }, + "gcp_service": { + "type": "string", + "title": "Gcp Service", + "default": "" + }, + "gcp_region": { + "type": "string", + "title": "Gcp Region", + "default": "" + }, + "gcp_sku_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id" + }, + "gcp_sku_id_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id Cud" + }, + "gcp_sku_id_3yr_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id 3Yr Cud" + }, + "gcp_ondemand_unit_price": { + "type": "number", + "title": "Gcp Ondemand Unit Price", + "default": 0.0 + }, + "gcp_1yr_cud_unit_price": { + "type": "number", + "title": "Gcp 1Yr Cud Unit Price", + "default": 0.0 + }, + "gcp_3yr_cud_unit_price": { + "type": "number", + "title": "Gcp 3Yr Cud Unit Price", + "default": 0.0 + }, + "gcp_quantity": { + "type": "number", + "title": "Gcp Quantity", + "default": 0.0 + }, + "gcp_usage_unit": { + "type": "string", + "title": "Gcp Usage Unit", + "default": "hour" + }, + "is_gcp_advantage": { + "type": "boolean", + "title": "Is Gcp Advantage", + "default": false + }, + "notes": { + "type": "string", + "title": "Notes", + "default": "" + } + }, + "type": "object", + "required": [ + "aws_service", + "aws_region", + "aws_monthly_cost" + ], + "title": "LineItemCreate", + "description": "Schema for adding a line item." + }, + "LineItemUpdate": { + "properties": { + "service_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Service Category" + }, + "aws_service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Service" + }, + "aws_region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Region" + }, + "aws_monthly_cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Aws Monthly Cost" + }, + "aws_sku": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Sku" + }, + "aws_unit_price": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Aws Unit Price" + }, + "aws_quantity": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Aws Quantity" + }, + "aws_usage_unit": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aws Usage Unit" + }, + "gcp_service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Service" + }, + "gcp_region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Region" + }, + "gcp_sku_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id" + }, + "gcp_sku_id_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id Cud" + }, + "gcp_sku_id_3yr_cud": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Sku Id 3Yr Cud" + }, + "gcp_ondemand_unit_price": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp Ondemand Unit Price" + }, + "gcp_1yr_cud_unit_price": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp 1Yr Cud Unit Price" + }, + "gcp_3yr_cud_unit_price": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp 3Yr Cud Unit Price" + }, + "gcp_quantity": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp Quantity" + }, + "gcp_usage_unit": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gcp Usage Unit" + }, + "gcp_ondemand_cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp Ondemand Cost" + }, + "gcp_1yr_cud_cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp 1Yr Cud Cost" + }, + "gcp_3yr_cud_cost": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Gcp 3Yr Cud Cost" + }, + "is_gcp_advantage": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Gcp Advantage" + }, + "is_excluded": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Excluded" + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "title": "LineItemUpdate", + "description": "Schema for updating a line item." + }, + "MCPServerCreate": { + "properties": { + "scope": { + "$ref": "#/components/schemas/MCPServerScope", + "description": "Scope level (organization or user)", + "default": "organization" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Unique MCP server name" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Human-readable display name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of MCP server" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon", + "description": "Icon identifier" + }, + "type": { + "$ref": "#/components/schemas/MCPServerType", + "description": "Server communication type" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Server URL (required for sse/http)" + }, + "headers": { + "items": { + "$ref": "#/components/schemas/MCPServerEnvVar" + }, + "type": "array", + "title": "Headers", + "description": "HTTP headers for sse/http (use ${credential:name} for sensitive data)" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "type" + ], + "title": "MCPServerCreate", + "description": "Schema for creating a new MCP server" + }, + "MCPServerEnvVar": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "Environment variable name" + }, + "value": { + "type": "string", + "title": "Value", + "description": "Environment variable value (use ${credential:name} for sensitive data)" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "title": "MCPServerEnvVar", + "description": "Individual environment variable" + }, + "MCPServerExport": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon" + }, + "type": { + "type": "string", + "title": "Type" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Headers" + }, + "credential_references": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Credential References" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "type" + ], + "title": "MCPServerExport", + "description": "MCP server data for export (no IDs, no secrets)" + }, + "MCPServerResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "scope": { + "$ref": "#/components/schemas/MCPServerScope" + }, + "organization_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Organization Id" + }, + "owner_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Owner Email" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon" + }, + "type": { + "$ref": "#/components/schemas/MCPServerType" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Headers" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "can_edit": { + "type": "boolean", + "title": "Can Edit", + "description": "Whether current user can edit this MCP", + "default": false + }, + "can_delete": { + "type": "boolean", + "title": "Can Delete", + "description": "Whether current user can delete this MCP", + "default": false + } + }, + "type": "object", + "required": [ + "id", + "scope", + "name", + "display_name", + "type", + "created_by_email", + "created_at", + "updated_at" + ], + "title": "MCPServerResponse", + "description": "Schema for MCP server API responses" + }, + "MCPServerScope": { + "type": "string", + "enum": [ + "organization", + "user" + ], + "title": "MCPServerScope", + "description": "MCP server scope levels" + }, + "MCPServerTestRequest": { + "properties": { + "type": { + "$ref": "#/components/schemas/MCPServerType", + "description": "MCP server type (sse or http)" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Server URL (required for sse/http)" + }, + "headers": { + "items": { + "$ref": "#/components/schemas/MCPServerEnvVar" + }, + "type": "array", + "title": "Headers", + "description": "HTTP headers (for sse/http)" + } + }, + "type": "object", + "required": [ + "type" + ], + "title": "MCPServerTestRequest", + "description": "Request model for testing MCP server connection" + }, + "MCPServerTestResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success", + "description": "Whether connection test succeeded" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error", + "description": "Error message if test failed" + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "List of available tool names" + }, + "tool_count": { + "type": "integer", + "title": "Tool Count", + "description": "Number of available tools", + "default": 0 + } + }, + "type": "object", + "required": [ + "success" + ], + "title": "MCPServerTestResponse", + "description": "Response model for MCP server test" + }, + "MCPServerType": { + "type": "string", + "enum": [ + "sse", + "http" + ], + "title": "MCPServerType", + "description": "MCP server communication types.\n\nOnly remote transports are supported. Local ``stdio`` (subprocess) servers\nwere removed: letting tenants define an arbitrary command/args to execute\ninside the agent pod was an OS command-injection / RCE vector." + }, + "MCPServerUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "Human-readable display name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of MCP server" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon", + "description": "Icon identifier" + }, + "type": { + "anyOf": [ + { + "$ref": "#/components/schemas/MCPServerType" + }, + { + "type": "null" + } + ], + "description": "Server communication type" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Server URL (sse/http only)" + }, + "headers": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MCPServerEnvVar" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "HTTP headers for sse/http (use ${credential:name} for sensitive data)" + } + }, + "type": "object", + "title": "MCPServerUpdate", + "description": "Schema for updating an existing MCP server" + }, + "MattermostChannel": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Channel ID" + }, + "team_id": { + "type": "string", + "title": "Team Id", + "description": "Team ID this channel belongs to" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Channel name (URL-safe identifier)" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Human-readable display name" + }, + "type": { + "type": "string", + "title": "Type", + "description": "Channel type (O=public, P=private, D=direct, G=group)", + "default": "O" + }, + "header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Header", + "description": "Channel header/topic" + }, + "purpose": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Purpose", + "description": "Channel purpose" + }, + "total_msg_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Msg Count", + "description": "Total messages in channel" + } + }, + "type": "object", + "required": [ + "id", + "team_id", + "name", + "display_name" + ], + "title": "MattermostChannel", + "description": "Mattermost channel information." + }, + "MattermostChannelConfig": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Config ID" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id", + "description": "Internal workspace ID" + }, + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "ChatIntegration ID" + }, + "integration_name": { + "type": "string", + "title": "Integration Name", + "description": "Integration display name" + }, + "provider_workspace_name": { + "type": "string", + "title": "Provider Workspace Name", + "description": "Mattermost server URL or name" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Mattermost channel ID" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel display name" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name", + "description": "Facets environment to filter incidents" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + }, + "is_paused": { + "type": "boolean", + "title": "Is Paused", + "description": "Whether incident creation is paused", + "default": false + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp" + } + }, + "type": "object", + "required": [ + "id", + "workspace_id", + "integration_id", + "integration_name", + "provider_workspace_name", + "channel_id", + "channel_name", + "created_at" + ], + "title": "MattermostChannelConfig", + "description": "Workspace-level Mattermost channel configuration for incident detection.\n\nSimilar to IncidentChannelConfig but for display purposes." + }, + "MattermostChannelListResponse": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/MattermostChannel" + }, + "type": "array", + "title": "Channels" + } + }, + "type": "object", + "required": [ + "channels" + ], + "title": "MattermostChannelListResponse", + "description": "Response containing list of Mattermost channels." + }, + "MattermostConfigureRequest": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "ChatIntegration ID (Mattermost)" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Mattermost channel ID" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel display name" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + } + }, + "type": "object", + "required": [ + "integration_id", + "channel_id", + "channel_name" + ], + "title": "MattermostConfigureRequest", + "description": "Request to configure Mattermost channel for a workspace." + }, + "MattermostIntegrationCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-given name for this integration" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "Mattermost server URL (e.g., https://mattermost.example.com)" + }, + "access_token": { + "type": "string", + "title": "Access Token", + "description": "Personal Access Token (PAT)" + }, + "webhook_token": { + "type": "string", + "title": "Webhook Token", + "description": "Outgoing webhook token for event validation" + } + }, + "type": "object", + "required": [ + "name", + "server_url", + "access_token", + "webhook_token" + ], + "title": "MattermostIntegrationCreate", + "description": "Schema for creating/connecting a Mattermost integration." + }, + "MattermostPauseRequest": { + "properties": { + "is_paused": { + "type": "boolean", + "title": "Is Paused", + "description": "True to pause, False to resume" + } + }, + "type": "object", + "required": [ + "is_paused" + ], + "title": "MattermostPauseRequest", + "description": "Request body for pausing/resuming incident responder." + }, + "MattermostSendPayload": { + "properties": { + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Mattermost channel ID" + }, + "message": { + "type": "string", + "title": "Message", + "description": "Message text (supports Markdown)" + }, + "root_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Root Id", + "description": "Root post ID for threading" + }, + "props": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Props", + "description": "Additional post properties" + } + }, + "type": "object", + "required": [ + "channel_id", + "message" + ], + "title": "MattermostSendPayload", + "description": "Mattermost create post payload." + }, + "MattermostTestResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "success" + ], + "title": "MattermostTestResponse", + "description": "Response from connection test." + }, + "MemoryAudience": { + "type": "string", + "enum": [ + "agent", + "user", + "org" + ], + "title": "MemoryAudience", + "description": "Who benefits from this memory \u2014 drives the (org, user, agent) cell\nthe row is stored in. Distinct from `kind`, which describes WHAT the\nmemory is about. We deliberately picked `audience` over `scope` to\navoid colliding with `kind=user` (which means \"ABOUT the user\")." + }, + "MemoryCategory": { + "type": "string", + "enum": [ + "fact", + "preference", + "decision", + "context", + "instruction" + ], + "title": "MemoryCategory", + "description": "Legacy taxonomy \u2014 kept for back-compat. New code should read `kind`." + }, + "MemoryCreate": { + "properties": { + "title": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Title", + "description": "Friendly title \u2014 Claude frontmatter `name`" + }, + "slug": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Slug", + "description": "Filesystem-safe identifier. Auto-derived from title via slugify() when omitted." + }, + "content": { + "type": "string", + "maxLength": 5000, + "minLength": 1, + "title": "Content", + "description": "The memory content to store" + }, + "summary": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ], + "title": "Summary", + "description": "Short description (Claude frontmatter `description`)" + }, + "index_hook": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ], + "title": "Index Hook", + "description": "MEMORY.md hook line \u2014 keyword form" + }, + "kind": { + "$ref": "#/components/schemas/MemoryKind", + "description": "Memory purpose taxonomy", + "default": "user" + }, + "audience": { + "$ref": "#/components/schemas/MemoryAudience", + "description": "Who benefits \u2014 agent | user | org. Defaults to user (safest cell when uncertain).", + "default": "user" + }, + "category": { + "$ref": "#/components/schemas/MemoryCategory", + "description": "Legacy category \u2014 kept for manual-UI back-compat", + "default": "fact" + }, + "importance": { + "$ref": "#/components/schemas/MemoryImportance", + "description": "Importance level", + "default": "medium" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "Searchable tags" + } + }, + "type": "object", + "required": [ + "title", + "content" + ], + "title": "MemoryCreate", + "description": "Schema for creating a new memory (used by MCP tool + REST)" + }, + "MemoryImportance": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "title": "MemoryImportance", + "description": "Importance levels for memory prioritization" + }, + "MemoryKind": { + "type": "string", + "enum": [ + "user", + "feedback", + "project", + "reference" + ], + "title": "MemoryKind", + "description": "Purpose-driven taxonomy mirroring Claude Code auto-memory frontmatter.\nSource of truth going forward; `MemoryCategory` is retained for back-compat." + }, + "MemoryRecallRequest": { + "properties": { + "query": { + "type": "string", + "minLength": 1, + "title": "Query", + "description": "Search query for semantic matching" + }, + "limit": { + "type": "integer", + "maximum": 20.0, + "minimum": 1.0, + "title": "Limit", + "description": "Maximum number of memories to return", + "default": 5 + }, + "category": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryCategory" + }, + { + "type": "null" + } + ], + "description": "Filter by category" + }, + "min_importance": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryImportance" + }, + { + "type": "null" + } + ], + "description": "Minimum importance level" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Filter by tags (any match)" + } + }, + "type": "object", + "required": [ + "query" + ], + "title": "MemoryRecallRequest", + "description": "Schema for recalling memories (semantic search)" + }, + "MemoryResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "slug": { + "type": "string", + "title": "Slug" + }, + "content": { + "type": "string", + "title": "Content" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary" + }, + "index_hook": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Index Hook" + }, + "kind": { + "$ref": "#/components/schemas/MemoryKind" + }, + "audience": { + "$ref": "#/components/schemas/MemoryAudience", + "default": "user" + }, + "category": { + "$ref": "#/components/schemas/MemoryCategory" + }, + "importance": { + "$ref": "#/components/schemas/MemoryImportance" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "source_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Session Id" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Accessed At" + }, + "access_count": { + "type": "integer", + "title": "Access Count" + }, + "relevance_score": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Relevance Score", + "description": "Relevance score from vector search (0-1)" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "title", + "slug", + "content", + "summary", + "kind", + "category", + "importance", + "tags", + "source_session_id", + "is_active", + "created_at", + "updated_at", + "last_accessed_at", + "access_count" + ], + "title": "MemoryResponse", + "description": "API response model for memories" + }, + "MemoryStats": { + "properties": { + "total_memories": { + "type": "integer", + "title": "Total Memories", + "description": "Total number of active memories" + }, + "by_category": { + "additionalProperties": true, + "type": "object", + "title": "By Category", + "description": "Count by category" + }, + "by_importance": { + "additionalProperties": true, + "type": "object", + "title": "By Importance", + "description": "Count by importance" + }, + "oldest_memory": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Oldest Memory" + }, + "newest_memory": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Newest Memory" + }, + "most_accessed": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Most Accessed", + "description": "ID of most frequently accessed memory" + } + }, + "type": "object", + "required": [ + "total_memories" + ], + "title": "MemoryStats", + "description": "Statistics about an agent's memories" + }, + "MemoryUpdate": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string", + "maxLength": 200, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "slug": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ], + "title": "Slug" + }, + "content": { + "anyOf": [ + { + "type": "string", + "maxLength": 5000, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "summary": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ], + "title": "Summary" + }, + "index_hook": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ], + "title": "Index Hook" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryKind" + }, + { + "type": "null" + } + ] + }, + "audience": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryAudience" + }, + { + "type": "null" + } + ] + }, + "category": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryCategory" + }, + { + "type": "null" + } + ] + }, + "importance": { + "anyOf": [ + { + "$ref": "#/components/schemas/MemoryImportance" + }, + { + "type": "null" + } + ] + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + } + }, + "type": "object", + "title": "MemoryUpdate", + "description": "Schema for updating an existing memory" + }, + "MigrationTarget": { + "type": "string", + "enum": [ + "aws", + "gcp", + "azure" + ], + "title": "MigrationTarget", + "description": "Target cloud for migration via Facets.\n\nDetermines which Facets module flavors to use when mapping\ndiscovered resources (from any source cloud)." + }, + "MigrationType": { + "type": "string", + "enum": [ + "aws_to_gcp" + ], + "title": "MigrationType", + "description": "Type of cloud migration." + }, + "ModuleStatus": { + "type": "string", + "enum": [ + "exists_in_project_type", + "exists_in_reference", + "needs_new_module" + ], + "title": "ModuleStatus", + "description": "Status of Facets module availability." + }, + "NewRelicIntegrationCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Name" + }, + "user_key": { + "type": "string", + "minLength": 1, + "title": "User Key", + "description": "New Relic user API key" + }, + "account_id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "title": "Account Id" + }, + "region": { + "$ref": "#/components/schemas/NewRelicRegion", + "default": "US" + } + }, + "type": "object", + "required": [ + "name", + "user_key", + "account_id" + ], + "title": "NewRelicIntegrationCreate", + "description": "Request body for creating a New Relic integration." + }, + "NewRelicIntegrationListResponse": { + "properties": { + "integrations": { + "items": { + "$ref": "#/components/schemas/NewRelicIntegrationResponse" + }, + "type": "array", + "title": "Integrations" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "integrations", + "total" + ], + "title": "NewRelicIntegrationListResponse", + "description": "Response for listing New Relic integrations." + }, + "NewRelicIntegrationResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "account_id": { + "type": "string", + "title": "Account Id" + }, + "region": { + "$ref": "#/components/schemas/NewRelicRegion" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "validated": { + "type": "boolean", + "title": "Validated" + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "integration_id", + "organization_id", + "name", + "account_id", + "region", + "is_active", + "validated", + "created_at", + "updated_at" + ], + "title": "NewRelicIntegrationResponse", + "description": "API response for a New Relic integration - NEVER includes encrypted_user_key." + }, + "NewRelicIntegrationUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string", + "maxLength": 200, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + }, + "user_key": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "User Key" + }, + "account_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Account Id" + }, + "region": { + "anyOf": [ + { + "$ref": "#/components/schemas/NewRelicRegion" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "NewRelicIntegrationUpdate", + "description": "Request body for updating a New Relic integration." + }, + "NewRelicIntegrationValidation": { + "properties": { + "valid": { + "type": "boolean", + "title": "Valid" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "valid" + ], + "title": "NewRelicIntegrationValidation", + "description": "Response for credential validation." + }, + "NewRelicRegion": { + "type": "string", + "enum": [ + "US", + "EU" + ], + "title": "NewRelicRegion" + }, + "OpenQuestionResponse": { + "properties": { + "question_id": { + "type": "string", + "title": "Question Id" + }, + "is_multi": { + "type": "boolean", + "title": "Is Multi" + }, + "payload": { + "additionalProperties": true, + "type": "object", + "title": "Payload" + } + }, + "type": "object", + "required": [ + "question_id", + "is_multi", + "payload" + ], + "title": "OpenQuestionResponse", + "description": "A WebSocket AskUserQuestion that's still awaiting an answer." + }, + "OrgPlanResponse": { + "properties": { + "dollar_limit": { + "type": "number", + "title": "Dollar Limit" + }, + "used_dollars": { + "type": "number", + "title": "Used Dollars" + }, + "cycle_key": { + "type": "string", + "title": "Cycle Key" + }, + "org_type": { + "type": "string", + "title": "Org Type" + } + }, + "type": "object", + "required": [ + "dollar_limit", + "used_dollars", + "cycle_key", + "org_type" + ], + "title": "OrgPlanResponse" + }, + "OutboundConnection": { + "properties": { + "target_repo_id": { + "type": "string", + "title": "Target Repo Id" + }, + "target_repo_name": { + "type": "string", + "title": "Target Repo Name" + }, + "connection_type": { + "type": "string", + "title": "Connection Type" + }, + "evidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence" + }, + "confidence": { + "type": "number", + "title": "Confidence" + } + }, + "type": "object", + "required": [ + "target_repo_id", + "target_repo_name", + "connection_type", + "evidence", + "confidence" + ], + "title": "OutboundConnection", + "description": "An edge from the requested repo to another repo, with the target name resolved." + }, + "PauseRequest": { + "properties": { + "is_paused": { + "type": "boolean", + "title": "Is Paused", + "description": "True to pause, False to resume" + } + }, + "type": "object", + "required": [ + "is_paused" + ], + "title": "PauseRequest", + "description": "Request body for pausing/resuming incident responder." + }, + "PlanChangeInfo": { + "properties": { + "is_scheduled": { + "type": "boolean", + "title": "Is Scheduled" + }, + "new_price_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "New Price Id" + }, + "new_dollar_limit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "New Dollar Limit" + }, + "change_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Change At" + } + }, + "type": "object", + "required": [ + "is_scheduled" + ], + "title": "PlanChangeInfo", + "description": "Plan change status information." + }, + "ProjectCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Project name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Optional description" + }, + "migration_type": { + "$ref": "#/components/schemas/MigrationType", + "description": "Migration type", + "default": "aws_to_gcp" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "ProjectCreate", + "description": "Schema for creating a migration project." + }, + "ProjectListItem": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "cur_status": { + "$ref": "#/components/schemas/CurMappingStatus" + }, + "status": { + "$ref": "#/components/schemas/CurMappingStatus" + }, + "migration_type": { + "$ref": "#/components/schemas/MigrationType" + }, + "aws_total": { + "type": "number", + "title": "Aws Total" + }, + "gcp_1yr_cud_total": { + "type": "number", + "title": "Gcp 1Yr Cud Total" + }, + "gcp_3yr_cud_total": { + "type": "number", + "title": "Gcp 3Yr Cud Total", + "default": 0.0 + }, + "savings_percent_1yr": { + "type": "number", + "title": "Savings Percent 1Yr" + }, + "savings_percent_3yr": { + "type": "number", + "title": "Savings Percent 3Yr", + "default": 0.0 + }, + "line_items_count": { + "type": "integer", + "title": "Line Items Count" + }, + "files_count": { + "type": "integer", + "title": "Files Count" + }, + "discovery_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Discovery Status" + }, + "k8s_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Status" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "cur_status", + "status", + "migration_type", + "aws_total", + "gcp_1yr_cud_total", + "savings_percent_1yr", + "line_items_count", + "files_count", + "created_at", + "updated_at" + ], + "title": "ProjectListItem", + "description": "Summarized project for list view." + }, + "ProjectListResponse": { + "properties": { + "projects": { + "items": { + "$ref": "#/components/schemas/ProjectListItem" + }, + "type": "array", + "title": "Projects" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "projects", + "total" + ], + "title": "ProjectListResponse", + "description": "API response for project list." + }, + "ProjectResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "cur_status": { + "$ref": "#/components/schemas/CurMappingStatus" + }, + "status": { + "$ref": "#/components/schemas/CurMappingStatus" + }, + "migration_type": { + "$ref": "#/components/schemas/MigrationType" + }, + "source_files": { + "items": { + "$ref": "#/components/schemas/SourceFile" + }, + "type": "array", + "title": "Source Files" + }, + "line_items": { + "items": { + "$ref": "#/components/schemas/LineItem" + }, + "type": "array", + "title": "Line Items" + }, + "aws_total": { + "type": "number", + "title": "Aws Total" + }, + "gcp_ondemand_total": { + "type": "number", + "title": "Gcp Ondemand Total" + }, + "gcp_1yr_cud_total": { + "type": "number", + "title": "Gcp 1Yr Cud Total" + }, + "gcp_3yr_cud_total": { + "type": "number", + "title": "Gcp 3Yr Cud Total" + }, + "gcp_advantages_total": { + "type": "number", + "title": "Gcp Advantages Total" + }, + "history": { + "items": { + "$ref": "#/components/schemas/entities__migration__migration_models__HistoryEntry" + }, + "type": "array", + "title": "History" + }, + "analysis_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Analysis Error" + }, + "chat_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Chat Session Id" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Email" + }, + "savings_percent_ondemand": { + "type": "number", + "title": "Savings Percent Ondemand", + "default": 0.0 + }, + "savings_percent_1yr": { + "type": "number", + "title": "Savings Percent 1Yr", + "default": 0.0 + }, + "savings_percent_3yr": { + "type": "number", + "title": "Savings Percent 3Yr", + "default": 0.0 + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "description", + "cur_status", + "status", + "migration_type", + "source_files", + "line_items", + "aws_total", + "gcp_ondemand_total", + "gcp_1yr_cud_total", + "gcp_3yr_cud_total", + "gcp_advantages_total", + "history", + "analysis_error", + "chat_session_id", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "ProjectResponse", + "description": "API response for a migration project." + }, + "ProjectUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "title": "ProjectUpdate", + "description": "Schema for updating a migration project." + }, + "QueryRequest": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + } + }, + "type": "object", + "required": [ + "message", + "agent_name" + ], + "title": "QueryRequest", + "description": "Request model for query mode endpoint." + }, + "QueryResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "messages": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Messages" + }, + "full_reply": { + "type": "string", + "title": "Full Reply" + }, + "tool_calls": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Tool Calls" + }, + "tool_results": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Tool Results" + }, + "usage": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Usage" + } + }, + "type": "object", + "required": [ + "session_id", + "messages", + "full_reply", + "tool_calls", + "tool_results", + "usage" + ], + "title": "QueryResponse", + "description": "Response model for query mode endpoint." + }, + "RelatedOpenIncidentItem": { + "properties": { + "incident": { + "$ref": "#/components/schemas/IncidentResponse" + }, + "direction": { + "type": "string", + "enum": [ + "upstream", + "downstream" + ], + "title": "Direction" + }, + "edge_type": { + "type": "string", + "title": "Edge Type" + }, + "hops": { + "type": "integer", + "title": "Hops" + } + }, + "type": "object", + "required": [ + "incident", + "direction", + "edge_type", + "hops" + ], + "title": "RelatedOpenIncidentItem", + "description": "One related open incident \u2014 IncidentResponse plus catalog-relation metadata." + }, + "RelatedOpenIncidentsResponse": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RelatedOpenIncidentItem" + }, + "type": "array", + "title": "Items" + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "RelatedOpenIncidentsResponse", + "description": "API response for related open incidents (catalog neighbors).\n\nEmpty ``items`` covers both \"no catalog entry for service\" and \"catalog\nentry exists but no neighbors have open incidents\" \u2014 observably identical\nto the UI, which hides the section in both cases." + }, + "RepoKBDetailResponse": { + "properties": { + "repo_id": { + "type": "string", + "title": "Repo Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "facts": { + "$ref": "#/components/schemas/RepoKBFacts" + }, + "overview": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Overview" + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Notes" + }, + "signals": { + "items": { + "$ref": "#/components/schemas/RepoSignal" + }, + "type": "array", + "title": "Signals" + }, + "content": { + "type": "string", + "title": "Content" + }, + "enrich_status": { + "type": "string", + "title": "Enrich Status" + }, + "indexed_commit_sha": { + "type": "string", + "title": "Indexed Commit Sha" + }, + "indexed_at": { + "type": "string", + "format": "date-time", + "title": "Indexed At" + }, + "populate_status": { + "type": "string", + "title": "Populate Status" + }, + "populate_session_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Populate Session Key" + }, + "connections_out": { + "items": { + "$ref": "#/components/schemas/OutboundConnection" + }, + "type": "array", + "title": "Connections Out" + }, + "connections_in": { + "items": { + "$ref": "#/components/schemas/InboundConnection" + }, + "type": "array", + "title": "Connections In" + } + }, + "type": "object", + "required": [ + "repo_id", + "name", + "facts", + "overview", + "notes", + "signals", + "content", + "enrich_status", + "indexed_commit_sha", + "indexed_at", + "populate_status", + "populate_session_key", + "connections_out", + "connections_in" + ], + "title": "RepoKBDetailResponse" + }, + "RepoKBFacts": { + "properties": { + "languages": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Languages" + }, + "build_cmd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Build Cmd" + }, + "test_cmd": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Test Cmd" + }, + "entrypoints": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Entrypoints" + } + }, + "type": "object", + "required": [ + "languages", + "build_cmd", + "test_cmd", + "entrypoints" + ], + "title": "RepoKBFacts", + "description": "Parsed fact fields from the KB document frontmatter." + }, + "RepoKBListItem": { + "properties": { + "repo_id": { + "type": "string", + "title": "Repo Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "status": { + "type": "string", + "title": "Status" + }, + "indexed_commit_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Indexed Commit Sha" + }, + "indexed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Indexed At" + }, + "populate_status": { + "type": "string", + "title": "Populate Status" + } + }, + "type": "object", + "required": [ + "repo_id", + "name", + "url", + "status", + "indexed_commit_sha", + "indexed_at", + "populate_status" + ], + "title": "RepoKBListItem", + "description": "One row in the list response." + }, + "RepoKBListResponse": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/RepoKBListItem" + }, + "type": "array", + "title": "Items" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "items", + "total" + ], + "title": "RepoKBListResponse" + }, + "RepoKBNoteRequest": { + "properties": { + "note": { + "type": "string", + "title": "Note" + } + }, + "type": "object", + "required": [ + "note" + ], + "title": "RepoKBNoteRequest" + }, + "RepoKBNoteResponse": { + "properties": { + "repo_id": { + "type": "string", + "title": "Repo Id" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "type": "object", + "required": [ + "repo_id", + "content" + ], + "title": "RepoKBNoteResponse" + }, + "RepoKBPopulateResponse": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "session_key": { + "type": "string", + "title": "Session Key" + } + }, + "type": "object", + "required": [ + "status", + "session_key" + ], + "title": "RepoKBPopulateResponse" + }, + "RepoRef": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "branch": { + "type": "string", + "title": "Branch", + "default": "main" + }, + "path": { + "type": "string", + "title": "Path", + "default": "/" + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "RepoRef", + "description": "Git repository reference." + }, + "RepoSignal": { + "properties": { + "kind": { + "type": "string", + "title": "Kind" + }, + "value": { + "type": "string", + "title": "Value" + }, + "evidence": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Evidence" + } + }, + "type": "object", + "required": [ + "kind", + "value" + ], + "title": "RepoSignal", + "description": "A current-state outbound signal extracted from a repo's checkout." + }, + "RepoUpdate": { + "properties": { + "app_repo": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepoRef" + }, + { + "type": "null" + } + ] + }, + "iac_repo": { + "anyOf": [ + { + "$ref": "#/components/schemas/RepoRef" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "RepoUpdate", + "description": "Request to set repos." + }, + "RepositoryAccessCheck": { + "properties": { + "repository_id": { + "type": "string", + "title": "Repository Id", + "description": "Repository ID to check" + }, + "pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Id", + "description": "PAT ID to check access with" + }, + "pat_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Name", + "description": "PAT name to check access with" + } + }, + "type": "object", + "required": [ + "repository_id" + ], + "title": "RepositoryAccessCheck", + "description": "Request model for checking repository access" + }, + "RepositoryAccessResult": { + "properties": { + "has_access": { + "type": "boolean", + "title": "Has Access", + "description": "Whether PAT has access to repository" + }, + "provider": { + "$ref": "#/components/schemas/GitProvider", + "description": "Git provider of the repository" + }, + "repository_name": { + "type": "string", + "title": "Repository Name", + "description": "Repository name" + }, + "repository_url": { + "type": "string", + "title": "Repository Url", + "description": "Repository URL" + }, + "pat_name": { + "type": "string", + "title": "Pat Name", + "description": "PAT name that was checked" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "Error message if access check failed" + }, + "checked_at": { + "type": "string", + "format": "date-time", + "title": "Checked At" + } + }, + "type": "object", + "required": [ + "has_access", + "provider", + "repository_name", + "repository_url", + "pat_name" + ], + "title": "RepositoryAccessResult", + "description": "Result of repository access check" + }, + "RepositoryConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Enable repository integration for this agent", + "default": false + }, + "repository_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Repository Id", + "description": "Link to repositories collection" + }, + "auth_mode": { + "type": "string", + "title": "Auth Mode", + "description": "Authentication mode: 'pat' or 'github_app_installation'", + "default": "pat" + }, + "default_pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Pat Id", + "description": "Default PAT to use for auto-checkout (can be overridden per session)" + }, + "branch": { + "type": "string", + "title": "Branch", + "description": "Default branch to checkout", + "default": "main" + }, + "auto_checkout": { + "type": "boolean", + "title": "Auto Checkout", + "description": "Automatically checkout repo when session starts", + "default": true + }, + "auto_pull": { + "type": "boolean", + "title": "Auto Pull", + "description": "Pull latest on session start", + "default": true + }, + "use_for_code": { + "type": "boolean", + "title": "Use For Code", + "description": "Repository used for code access (read/write, debug, review)", + "default": true + }, + "use_for_memory": { + "type": "boolean", + "title": "Use For Memory", + "description": "Repository used for long-term memory (git issues & files)", + "default": true + } + }, + "type": "object", + "title": "RepositoryConfig", + "description": "Repository integration configuration for custom agents" + }, + "RepositoryConfigExport": { + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": false + }, + "branch": { + "type": "string", + "title": "Branch", + "default": "main" + }, + "auto_checkout": { + "type": "boolean", + "title": "Auto Checkout", + "default": true + }, + "auto_pull": { + "type": "boolean", + "title": "Auto Pull", + "default": true + }, + "use_for_code": { + "type": "boolean", + "title": "Use For Code", + "default": true + }, + "use_for_memory": { + "type": "boolean", + "title": "Use For Memory", + "default": true + } + }, + "type": "object", + "title": "RepositoryConfigExport", + "description": "Repository config for export (no IDs)" + }, + "RepositoryCreate": { + "properties": { + "url": { + "type": "string", + "title": "Url", + "description": "Git repository URL" + }, + "auth_mode": { + "type": "string", + "title": "Auth Mode", + "description": "Authentication mode: 'pat' or 'github_app_installation'", + "default": "pat" + }, + "pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Id", + "description": "PAT ID to use for validation (required when auth_mode='pat')" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "Repository name (auto-extracted if not provided)" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Repository description" + }, + "default_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Branch", + "description": "Default branch to use (auto-detected from provider if not specified)" + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "RepositoryCreate", + "description": "Schema for creating a new repository" + }, + "RepositoryDiscoveryRequest": { + "properties": { + "auth_mode": { + "type": "string", + "title": "Auth Mode", + "description": "Authentication mode: 'pat' or 'github_app_installation'", + "default": "pat" + }, + "pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Pat Id", + "description": "PAT ID to use for discovery (required when auth_mode='pat')" + }, + "auto_add": { + "type": "boolean", + "title": "Auto Add", + "description": "Automatically add discovered repositories", + "default": false + } + }, + "type": "object", + "title": "RepositoryDiscoveryRequest", + "description": "Request model for discovering repositories via PAT or GitHub App installation" + }, + "RepositoryDiscoveryResponse": { + "properties": { + "discovered": { + "items": { + "$ref": "#/components/schemas/DiscoveredRepository" + }, + "type": "array", + "title": "Discovered" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "pat_provider": { + "$ref": "#/components/schemas/GitProvider" + }, + "already_added": { + "type": "integer", + "title": "Already Added", + "description": "Number of repos already in organization" + } + }, + "type": "object", + "required": [ + "discovered", + "total", + "pat_provider", + "already_added" + ], + "title": "RepositoryDiscoveryResponse", + "description": "Response model for repository discovery" + }, + "RepositoryExport": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "provider": { + "type": "string", + "title": "Provider" + }, + "default_branch": { + "type": "string", + "title": "Default Branch", + "default": "main" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": [ + "name", + "url", + "provider" + ], + "title": "RepositoryExport", + "description": "Repository data for export" + }, + "RepositoryImportInfo": { + "properties": { + "has_repository_config": { + "type": "boolean", + "title": "Has Repository Config", + "default": false + }, + "repository_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Repository Url" + }, + "repository_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Repository Name" + }, + "url_match_found": { + "type": "boolean", + "title": "Url Match Found", + "default": false + }, + "matched_repository_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Matched Repository Id" + }, + "matched_repository_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Matched Repository Name" + }, + "available_repositories": { + "items": { + "$ref": "#/components/schemas/RepositoryOption" + }, + "type": "array", + "title": "Available Repositories" + } + }, + "type": "object", + "title": "RepositoryImportInfo", + "description": "Information about repository during import" + }, + "RepositoryListResponse": { + "properties": { + "repositories": { + "items": { + "$ref": "#/components/schemas/RepositoryResponse" + }, + "type": "array", + "title": "Repositories" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "repositories", + "total" + ], + "title": "RepositoryListResponse", + "description": "Response model for listing repositories" + }, + "RepositoryOption": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "provider": { + "type": "string", + "title": "Provider" + } + }, + "type": "object", + "required": [ + "id", + "name", + "url", + "provider" + ], + "title": "RepositoryOption", + "description": "Repository option for import linking" + }, + "RepositoryResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "url": { + "type": "string", + "title": "Url" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "provider": { + "$ref": "#/components/schemas/GitProvider" + }, + "default_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Branch" + }, + "added_by_email": { + "type": "string", + "title": "Added By Email" + }, + "added_via_pat_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Added Via Pat Id" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_validated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Validated At" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "url", + "provider", + "added_by_email", + "created_at", + "updated_at" + ], + "title": "RepositoryResponse", + "description": "Schema for repository API responses" + }, + "RepositoryUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "default_branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Branch" + } + }, + "type": "object", + "title": "RepositoryUpdate", + "description": "Schema for updating repository metadata" + }, + "RescanRequest": { + "properties": { + "user_instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Instructions" + } + }, + "type": "object", + "title": "RescanRequest", + "description": "Request body for individual entry rescan." + }, + "ResourceTypeCount": { + "properties": { + "resource_type": { + "type": "string", + "title": "Resource Type", + "description": "e.g., 'eks_cluster', 'rds_instance', 'aks_cluster', 'gke_cluster'" + }, + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "Source cloud provider" + }, + "count": { + "type": "integer", + "title": "Count", + "description": "Number of resources", + "default": 0 + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Regions", + "description": "Regions with resources" + }, + "sample_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Sample Names", + "description": "Sample resource names (up to 5)" + }, + "discovered_resources": { + "items": { + "$ref": "#/components/schemas/DiscoveredResource" + }, + "type": "array", + "title": "Discovered Resources", + "description": "All discovered resources with their IDs and configs (for blueprint creation)" + }, + "network_bound": { + "type": "boolean", + "title": "Network Bound", + "description": "Whether type is VPC/VNet/VPC-bound", + "default": true + }, + "engines": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Engines", + "description": "DB engines (for RDS/Cloud SQL/Azure SQL)" + }, + "metadata": { + "additionalProperties": true, + "type": "object", + "title": "Metadata", + "description": "Type-specific metadata" + }, + "facets_intent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Intent", + "description": "Suggested Facets intent (e.g., kubernetes_cluster, postgres)" + }, + "facets_flavor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Flavor", + "description": "Suggested Facets flavor (e.g., eks, aurora)" + }, + "module_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module Version", + "description": "Module version if exists (e.g., 1.0.0)" + }, + "module_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module Status", + "description": "Module status: exists_in_project_type | exists_in_reference | needs_new_module" + }, + "module_file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module File Path", + "description": "Path to module in reference repo (for raptor publish)" + }, + "is_stub_module": { + "type": "boolean", + "title": "Is Stub Module", + "description": "True if module was auto-created as stub during scan (needs full implementation in DAG phase)", + "default": false + }, + "approved": { + "type": "boolean", + "title": "Approved", + "description": "Whether this resource type mapping has been approved", + "default": false + }, + "published": { + "type": "boolean", + "title": "Published", + "description": "Whether module has been published to project type", + "default": false + }, + "action_session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Action Session Id", + "description": "Session ID for publish/develop action (for viewing chat)" + }, + "action_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Action Status", + "description": "Action status: pending | running | completed | error" + }, + "action_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Action Type", + "description": "Action type: publish | develop" + }, + "action_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Action Error", + "description": "Error message if action failed" + }, + "validation_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Status", + "description": "Validation status: pending | valid | failed. For project_type modules: validated immediately on mapping. For reference/develop modules: validated after action completes." + }, + "validation_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Validation Error", + "description": "Validation error message if validation_status is failed" + } + }, + "type": "object", + "required": [ + "resource_type", + "provider" + ], + "title": "ResourceTypeCount", + "description": "Aggregated count of cloud resources by type (multi-cloud)." + }, + "ResourceTypeCountCreate": { + "properties": { + "resource_type": { + "type": "string", + "title": "Resource Type", + "description": "e.g., 'eks_cluster', 'rds_instance', 'aks_cluster'" + }, + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "Cloud provider (aws, azure, gcp)" + }, + "count": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Count", + "description": "Number of resources (required, must be > 0)" + }, + "regions": { + "items": { + "type": "string" + }, + "type": "array", + "minItems": 1, + "title": "Regions", + "description": "Regions where resources exist (at least one)" + }, + "sample_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Sample Names", + "description": "Sample resource names" + }, + "network_bound": { + "type": "boolean", + "title": "Network Bound", + "description": "Whether type is VPC/VNet bound", + "default": true + }, + "engines": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Engines", + "description": "DB engines (for database types)" + }, + "facets_intent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Intent", + "description": "Suggested Facets intent (e.g., kubernetes_cluster, postgres)" + }, + "facets_flavor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Flavor", + "description": "Suggested Facets flavor (e.g., eks, aurora)" + }, + "module_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module Version", + "description": "Module version if exists (e.g., 1.0.0)" + }, + "module_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module Status", + "description": "Module status: exists_in_project_type | exists_in_reference | needs_new_module" + }, + "module_file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module File Path", + "description": "Path to module in reference repo (for raptor publish)" + }, + "approved": { + "type": "boolean", + "title": "Approved", + "description": "Whether this resource type mapping has been approved", + "default": false + }, + "published": { + "type": "boolean", + "title": "Published", + "description": "Whether module has been published to project type", + "default": false + } + }, + "type": "object", + "required": [ + "resource_type", + "provider", + "count", + "regions" + ], + "title": "ResourceTypeCountCreate", + "description": "Request to manually add a resource type count." + }, + "ResourceTypeMapping": { + "properties": { + "resource_type": { + "type": "string", + "title": "Resource Type", + "description": "Cloud resource type (e.g., eks_cluster, aks_cluster)" + }, + "provider": { + "$ref": "#/components/schemas/CloudProvider", + "description": "Source cloud provider" + }, + "facets_intent": { + "type": "string", + "title": "Facets Intent", + "description": "Facets intent name" + }, + "facets_flavor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Flavor", + "description": "Facets flavor" + }, + "module_status": { + "$ref": "#/components/schemas/ModuleStatus", + "description": "Module availability status" + }, + "module_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Module Version", + "description": "Version if exists" + }, + "resource_count": { + "type": "integer", + "title": "Resource Count", + "description": "Number of resources of this type", + "default": 0 + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes", + "description": "Additional notes" + } + }, + "type": "object", + "required": [ + "resource_type", + "provider", + "facets_intent", + "module_status" + ], + "title": "ResourceTypeMapping", + "description": "Mapping from cloud resource type to Facets intent (multi-cloud)." + }, + "ResponseMode": { + "type": "string", + "enum": [ + "mentions_only", + "all_messages" + ], + "title": "ResponseMode", + "description": "When the agent processes and responds to messages." + }, + "RootCauseCount": { + "properties": { + "category": { + "type": "string", + "title": "Category" + }, + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "category", + "count" + ], + "title": "RootCauseCount", + "description": "Count of incidents by root cause category." + }, + "Rule": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "expression": { + "type": "string", + "title": "Expression" + }, + "threshold": { + "type": "string", + "title": "Threshold", + "default": "" + }, + "forDuration": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Forduration" + }, + "severity": { + "$ref": "#/components/schemas/entities__alert_doctor__alert_doctor_models__Severity", + "default": "warning" + } + }, + "type": "object", + "required": [ + "name", + "expression" + ], + "title": "Rule", + "description": "A single alerting rule (maps to a Prometheus alerting rule)." + }, + "ScanHistoryListResponse": { + "properties": { + "records": { + "items": { + "$ref": "#/components/schemas/ScanRecordResponse" + }, + "type": "array", + "title": "Records" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "records", + "total" + ], + "title": "ScanHistoryListResponse", + "description": "API response for listing scan records." + }, + "ScanRecordResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "scan_type": { + "type": "string", + "title": "Scan Type" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "integration_name": { + "type": "string", + "title": "Integration Name", + "default": "" + }, + "environment_label": { + "type": "string", + "title": "Environment Label", + "default": "" + }, + "session_key": { + "type": "string", + "title": "Session Key", + "default": "" + }, + "status": { + "type": "string", + "title": "Status" + }, + "started_at": { + "type": "string", + "format": "date-time", + "title": "Started At" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At" + }, + "execution_time_seconds": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Execution Time Seconds" + }, + "cost_usd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Cost Usd" + }, + "entries_created": { + "type": "integer", + "title": "Entries Created", + "default": 0 + }, + "entries_updated": { + "type": "integer", + "title": "Entries Updated", + "default": 0 + }, + "entries_unchanged": { + "type": "integer", + "title": "Entries Unchanged", + "default": 0 + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "triggered_by": { + "type": "string", + "title": "Triggered By", + "default": "" + }, + "target_entry_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Entry Id" + }, + "target_entry_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Target Entry Name" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "scan_type", + "integration_id", + "status", + "started_at", + "created_at", + "updated_at" + ], + "title": "ScanRecordResponse", + "description": "API response for a single scan record." + }, + "ScanRequest": { + "properties": { + "scan_type": { + "type": "string", + "title": "Scan Type" + }, + "integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Id" + }, + "workspace_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id" + }, + "environment_label": { + "type": "string", + "title": "Environment Label", + "default": "prod" + }, + "project_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Name" + }, + "environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Environment Name" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region" + }, + "user_instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Instructions" + } + }, + "type": "object", + "required": [ + "scan_type" + ], + "title": "ScanRequest", + "description": "Request to trigger a catalog scan." + }, + "ScanTargetUpdateRequest": { + "properties": { + "enabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enabled" + }, + "cron_expression": { + "anyOf": [ + { + "type": "string", + "pattern": "^[\\d\\*\\/\\-\\,\\s]+$" + }, + { + "type": "null" + } + ], + "title": "Cron Expression" + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone" + } + }, + "type": "object", + "title": "ScanTargetUpdateRequest", + "description": "Request body for updating a scan target." + }, + "ScheduleStatus": { + "type": "string", + "enum": [ + "active", + "paused", + "error" + ], + "title": "ScheduleStatus", + "description": "Schedule lifecycle status" + }, + "ScheduleTarget": { + "properties": { + "target_type": { + "$ref": "#/components/schemas/TargetType", + "description": "Type of infrastructure target" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Human-readable label for UI display" + }, + "k8s_source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Source Id", + "description": "K8sSource entity ID (org-level integration)" + }, + "k8s_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Integration Name", + "description": "K8s integration display name" + }, + "cloud_source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cloud Source Id", + "description": "CloudSource entity ID (org-level cloud integration)" + }, + "deployment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Name", + "description": "Kubernetes deployment/service name to monitor in this environment" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id", + "description": "FacetsIntegration entity ID" + }, + "facets_project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project", + "description": "Facets project name" + }, + "facets_environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment", + "description": "Facets environment name (e.g., 'staging')" + }, + "facets_environment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Id", + "description": "Facets CP environment ID" + }, + "has_k8s_cluster": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Has K8S Cluster", + "description": "Whether this Facets env has a K8s cluster" + } + }, + "type": "object", + "required": [ + "target_type", + "display_name" + ], + "title": "ScheduleTarget", + "description": "An infrastructure target for a scheduled agent run." + }, + "SendMessageRequest": { + "properties": { + "slack_payload": { + "anyOf": [ + { + "$ref": "#/components/schemas/SlackSendPayload" + }, + { + "type": "null" + } + ] + }, + "mattermost_payload": { + "anyOf": [ + { + "$ref": "#/components/schemas/MattermostSendPayload" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "SendMessageRequest", + "description": "Request body for sending a message via a chat integration.\n\nExactly one of slack_payload or mattermost_payload must be provided." + }, + "SendMessageResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "provider": { + "type": "string", + "title": "Provider" + }, + "provider_response": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Provider Response" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "success", + "provider" + ], + "title": "SendMessageResponse", + "description": "Response from the send-message endpoint." + }, + "ServiceCount": { + "properties": { + "service": { + "type": "string", + "title": "Service" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "open": { + "type": "integer", + "title": "Open" + }, + "avg_mtti_hours": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Avg Mtti Hours" + } + }, + "type": "object", + "required": [ + "service", + "total", + "open" + ], + "title": "ServiceCount", + "description": "Incident counts for a service." + }, + "SessionCreateRequest": { + "properties": { + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "agent_init_args": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Agent Init Args" + } + }, + "type": "object", + "required": [ + "agent_name" + ], + "title": "SessionCreateRequest" + }, + "SessionCreateResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "current_permission_mode": { + "type": "string", + "title": "Current Permission Mode", + "default": "default" + } + }, + "type": "object", + "required": [ + "session_id", + "agent_name" + ], + "title": "SessionCreateResponse" + }, + "SessionEventsResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "event_count": { + "type": "integer", + "title": "Event Count" + }, + "events": { + "items": { + "$ref": "#/components/schemas/UIEventResponse" + }, + "type": "array", + "title": "Events" + }, + "external_run_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Run Status" + }, + "external_run_started_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Run Started At" + }, + "external_run_completed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Run Completed At" + }, + "external_run_cancelled_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "External Run Cancelled At" + }, + "session_cost_usd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Session Cost Usd" + } + }, + "type": "object", + "required": [ + "session_id", + "event_count", + "events" + ], + "title": "SessionEventsResponse", + "description": "Response containing UI events for conversation replay." + }, + "SessionResumeResponse": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "session_valid": { + "type": "boolean", + "title": "Session Valid" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "current_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current Model", + "default": "intelligent" + }, + "repositories": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Repositories", + "default": [] + }, + "kubernetes": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Kubernetes" + }, + "current_permission_mode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current Permission Mode", + "default": "default" + }, + "is_read_only": { + "type": "boolean", + "title": "Is Read Only", + "default": false + }, + "is_shared": { + "type": "boolean", + "title": "Is Shared", + "default": false + }, + "session_cost_usd": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Session Cost Usd" + }, + "open_questions": { + "items": { + "$ref": "#/components/schemas/OpenQuestionResponse" + }, + "type": "array", + "title": "Open Questions", + "default": [] + } + }, + "type": "object", + "required": [ + "status", + "agent_name", + "session_valid" + ], + "title": "SessionResumeResponse" + }, + "SessionSummary": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "agent_name": { + "type": "string", + "title": "Agent Name" + }, + "current_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current Model" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "preview": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preview" + }, + "is_automated": { + "type": "boolean", + "title": "Is Automated", + "default": false + } + }, + "type": "object", + "required": [ + "session_id", + "title", + "agent_name", + "current_model", + "created_at", + "updated_at" + ], + "title": "SessionSummary", + "description": "Summary of a chat session for history list." + }, + "SessionsListResponse": { + "properties": { + "sessions": { + "items": { + "$ref": "#/components/schemas/SessionSummary" + }, + "type": "array", + "title": "Sessions" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "has_more": { + "type": "boolean", + "title": "Has More" + }, + "grouped": { + "$ref": "#/components/schemas/GroupedSessions" + } + }, + "type": "object", + "required": [ + "sessions", + "total", + "has_more", + "grouped" + ], + "title": "SessionsListResponse", + "description": "Response for listing user sessions." + }, + "SetAdminRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email" + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "SetAdminRequest" + }, + "SetOrgPlanRequest": { + "properties": { + "plan_tier": { + "type": "string", + "title": "Plan Tier" + }, + "cycle_day": { + "anyOf": [ + { + "type": "integer", + "maximum": 31.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Cycle Day" + }, + "dollar_limit": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Dollar Limit" + } + }, + "type": "object", + "required": [ + "plan_tier" + ], + "title": "SetOrgPlanRequest" + }, + "Severity-Output": { + "type": "string", + "enum": [ + "P1", + "P2", + "P3", + "P4", + "P5" + ], + "title": "Severity", + "description": "Incident severity level." + }, + "SeverityCount": { + "properties": { + "severity": { + "type": "string", + "title": "Severity" + }, + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "severity", + "count" + ], + "title": "SeverityCount", + "description": "Count of incidents by severity." + }, + "ShareSessionResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "session_id": { + "type": "string", + "title": "Session Id" + }, + "share_url": { + "type": "string", + "title": "Share Url" + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "success", + "session_id", + "share_url", + "message" + ], + "title": "ShareSessionResponse", + "description": "Response after sharing session with organization." + }, + "SkillCreate": { + "properties": { + "scope": { + "$ref": "#/components/schemas/SkillScope", + "description": "Skill scope", + "default": "organization" + }, + "surface": { + "$ref": "#/components/schemas/SkillSurface", + "description": "Runnable surface (cli, ui, both)", + "default": "both" + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "pattern": "^[a-z0-9_-]+$", + "title": "Name" + }, + "display_name": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "title": "Display Name" + }, + "description": { + "type": "string", + "maxLength": 2000, + "minLength": 10, + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon", + "default": "\ud83d\udcd6" + }, + "triggers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Triggers" + }, + "version": { + "type": "string", + "title": "Version", + "default": "1.0" + }, + "category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Category" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "content": { + "type": "string", + "maxLength": 200000, + "minLength": 50, + "title": "Content" + }, + "files": { + "items": { + "$ref": "#/components/schemas/SkillFile" + }, + "type": "array", + "title": "Files" + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "description", + "content" + ], + "title": "SkillCreate", + "description": "Schema for creating a new skill" + }, + "SkillFile": { + "properties": { + "path": { + "type": "string", + "maxLength": 255, + "minLength": 1, + "title": "Path", + "description": "Skill-relative POSIX path, e.g. 'nuances-catalog.md'" + }, + "content": { + "type": "string", + "title": "Content", + "description": "File body" + } + }, + "type": "object", + "required": [ + "path", + "content" + ], + "title": "SkillFile", + "description": "One supporting file of a multi-file (\"nested\") skill.\n\nA skill is a directory: ``SKILL.md`` lives in ``Skill.content`` and every\nother file in the skill folder is a ``SkillFile`` carried alongside. ``path``\nis the skill-relative POSIX path (e.g. ``nuances-catalog.md`` or\n``flows/onboard.md``); installers recreate it verbatim under the skill dir." + }, + "SkillImport": { + "properties": { + "markdown": { + "type": "string", + "minLength": 1, + "title": "Markdown", + "description": "Full skill markdown (frontmatter + body)" + }, + "scope": { + "$ref": "#/components/schemas/SkillScope", + "description": "Destination scope for the imported skill", + "default": "organization" + }, + "surface": { + "$ref": "#/components/schemas/SkillSurface", + "description": "Runnable surface for the imported skill (cli, ui, both)", + "default": "both" + } + }, + "type": "object", + "required": [ + "markdown" + ], + "title": "SkillImport", + "description": "Request body for the paste-markdown import endpoint.\n\nThe Knowledge \u2192 Skills page lets users paste a Claude-style skill\nmarkdown blob (YAML frontmatter + body). Only the markdown text and\nthe destination scope cross the wire \u2014 the route parses the\nfrontmatter and turns it into a SkillCreate." + }, + "SkillResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "scope": { + "$ref": "#/components/schemas/SkillScope" + }, + "surface": { + "$ref": "#/components/schemas/SkillSurface", + "default": "both" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "owner_email": { + "type": "string", + "title": "Owner Email" + }, + "name": { + "type": "string", + "title": "Name" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "icon": { + "type": "string", + "title": "Icon" + }, + "triggers": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Triggers" + }, + "version": { + "type": "string", + "title": "Version" + }, + "category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Category" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "content": { + "type": "string", + "title": "Content" + }, + "files": { + "items": { + "$ref": "#/components/schemas/SkillFile" + }, + "type": "array", + "title": "Files" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "can_edit": { + "type": "boolean", + "title": "Can Edit" + } + }, + "type": "object", + "required": [ + "id", + "scope", + "organization_id", + "owner_email", + "name", + "display_name", + "description", + "icon", + "triggers", + "version", + "category", + "tags", + "content", + "is_active", + "created_at", + "updated_at", + "can_edit" + ], + "title": "SkillResponse", + "description": "API response model for skills" + }, + "SkillScope": { + "type": "string", + "enum": [ + "global", + "organization", + "personal" + ], + "title": "SkillScope", + "description": "Scope of skill visibility" + }, + "SkillSurface": { + "type": "string", + "enum": [ + "cli", + "ui", + "both" + ], + "title": "SkillSurface", + "description": "Where a skill is meant to run \u2014 its *authored intent*, not tool-resolvability.\n\nThis is orthogonal to the CLI installability gate\n(``CLI_INSTALLABLE_GLOBAL_SKILLS`` in ``routes/cli_skills.py``): that gate\nanswers \"can this GLOBAL skill's tools be satisfied on a local host?\", while\n``surface`` answers \"which surface is this skill *for*?\".\n\n- ``CLI`` \u2014 local AI host only (drives local binaries like ``raptor`` / ``aws``\n or a local browser). Excluded from the server-hosted UI agent's skill sync\n (``anthropic_agents/skills/skill_sync.py``) because the UI has no terminal.\n- ``UI`` \u2014 server-hosted chat only. Excluded from the CLI ``/bundle``.\n- ``BOTH`` \u2014 runs on either surface (default; backward-compatible for every\n existing skill)." + }, + "SkillUpdate": { + "properties": { + "display_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ], + "title": "Display Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Icon" + }, + "triggers": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Triggers" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version" + }, + "category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Category" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags" + }, + "surface": { + "anyOf": [ + { + "$ref": "#/components/schemas/SkillSurface" + }, + { + "type": "null" + } + ] + }, + "content": { + "anyOf": [ + { + "type": "string", + "maxLength": 200000 + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "files": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/SkillFile" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Files" + } + }, + "type": "object", + "title": "SkillUpdate", + "description": "Schema for updating an existing skill (partial updates).\n\nPermissive on minimums and any field where existing rows could violate\na tighter Create-only constraint. Only max_length caps apply so legacy\nrows that violate Create-only minimums remain editable." + }, + "SlackBotCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-given name for this bot" + }, + "slack_integration_id": { + "type": "string", + "title": "Slack Integration Id", + "description": "Reference to ChatIntegration for Slack" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Slack channel ID" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Slack channel name" + }, + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "Reference to CustomAgent to run" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id", + "description": "Reference to FacetsIntegration" + }, + "facets_environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment", + "description": "Facets environment name" + }, + "response_mode": { + "$ref": "#/components/schemas/ResponseMode", + "description": "Response mode for conversations", + "default": "mentions_only" + } + }, + "type": "object", + "required": [ + "name", + "slack_integration_id", + "channel_id", + "channel_name", + "agent_id" + ], + "title": "SlackBotCreate", + "description": "Schema for creating a Slack Bot." + }, + "SlackBotListResponse": { + "properties": { + "bots": { + "items": { + "$ref": "#/components/schemas/SlackBotResponse" + }, + "type": "array", + "title": "Bots" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "bots", + "total" + ], + "title": "SlackBotListResponse", + "description": "API response for listing Slack Bots." + }, + "SlackBotResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "slack_integration_id": { + "type": "string", + "title": "Slack Integration Id" + }, + "slack_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Slack Integration Name" + }, + "channel_id": { + "type": "string", + "title": "Channel Id" + }, + "channel_name": { + "type": "string", + "title": "Channel Name" + }, + "agent_id": { + "type": "string", + "title": "Agent Id" + }, + "agent_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Name" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id" + }, + "facets_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Name" + }, + "facets_environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment" + }, + "response_mode": { + "$ref": "#/components/schemas/ResponseMode", + "default": "mentions_only" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_session_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Session At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "organization_id", + "slack_integration_id", + "channel_id", + "channel_name", + "agent_id", + "is_active", + "created_by_email", + "created_at", + "updated_at" + ], + "title": "SlackBotResponse", + "description": "API response for Slack Bot." + }, + "SlackBotUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "New name for the bot" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "New agent reference" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id", + "description": "New Facets integration reference" + }, + "facets_environment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment", + "description": "New Facets environment" + }, + "response_mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/ResponseMode" + }, + { + "type": "null" + } + ], + "description": "New response mode" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active", + "description": "Enable/disable bot" + } + }, + "type": "object", + "title": "SlackBotUpdate", + "description": "Schema for updating a Slack Bot." + }, + "SlackChannel": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Channel ID (C01234567)" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Channel name without #" + }, + "is_private": { + "type": "boolean", + "title": "Is Private", + "default": false + }, + "num_members": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Num Members" + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "SlackChannel", + "description": "Slack channel info for dropdown." + }, + "SlackChannelConfigResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "integration_name": { + "type": "string", + "title": "Integration Name" + }, + "provider_workspace_name": { + "type": "string", + "title": "Provider Workspace Name" + }, + "channel_id": { + "type": "string", + "title": "Channel Id" + }, + "channel_name": { + "type": "string", + "title": "Channel Name" + }, + "facets_integration_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Id" + }, + "facets_integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Integration Name" + }, + "facets_project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Project" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name" + }, + "has_k8s_cluster": { + "type": "boolean", + "title": "Has K8S Cluster", + "default": false + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + }, + "is_paused": { + "type": "boolean", + "title": "Is Paused", + "description": "Whether incident creation is paused", + "default": false + }, + "events_enabled": { + "type": "boolean", + "title": "Events Enabled", + "description": "Whether webhook events are enabled", + "default": true + } + }, + "type": "object", + "required": [ + "id", + "workspace_id", + "integration_id", + "integration_name", + "provider_workspace_name", + "channel_id", + "channel_name" + ], + "title": "SlackChannelConfigResponse", + "description": "Response for workspace Slack channel configuration." + }, + "SlackChannelListResponse": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/SlackChannel" + }, + "type": "array", + "title": "Channels" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "channels", + "total" + ], + "title": "SlackChannelListResponse", + "description": "API response for channel list." + }, + "SlackConfig": { + "properties": { + "chat_integration_id": { + "type": "string", + "title": "Chat Integration Id", + "description": "ChatIntegration entity ID (Slack workspace)" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Slack channel ID" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Slack channel display name" + } + }, + "type": "object", + "required": [ + "chat_integration_id", + "channel_id", + "channel_name" + ], + "title": "SlackConfig", + "description": "Slack integration configuration." + }, + "SlackInstallUrlResponse": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "webhook_id": { + "type": "string", + "title": "Webhook Id" + } + }, + "type": "object", + "required": [ + "url", + "webhook_id" + ], + "title": "SlackInstallUrlResponse", + "description": "Response from GET /integrations/slack/install-url." + }, + "SlackSendPayload": { + "properties": { + "channel": { + "type": "string", + "title": "Channel", + "description": "Slack channel ID (e.g., C1234567890)" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text", + "description": "Message text (fallback for blocks)" + }, + "blocks": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Blocks", + "description": "Block Kit blocks" + }, + "thread_ts": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Thread Ts", + "description": "Thread timestamp for replies" + }, + "reply_broadcast": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Reply Broadcast", + "description": "Also post to channel when replying in thread" + }, + "unfurl_links": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Unfurl Links", + "description": "Enable link unfurling" + }, + "unfurl_media": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Unfurl Media", + "description": "Enable media unfurling" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Event metadata" + }, + "mrkdwn": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Mrkdwn", + "description": "Enable mrkdwn formatting" + } + }, + "type": "object", + "required": [ + "channel" + ], + "title": "SlackSendPayload", + "description": "Whitelisted Slack chat.postMessage payload.\n\nOnly safe fields are accepted. Impersonation fields (as_user, username,\nicon_emoji, icon_url), token, and deprecated attachments are blocked." + }, + "SlackTestResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "message_ts": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message Ts" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "success" + ], + "title": "SlackTestResponse", + "description": "API response for test message." + }, + "SourceFile": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "filename": { + "type": "string", + "title": "Filename" + }, + "content_type": { + "type": "string", + "title": "Content Type" + }, + "size_bytes": { + "type": "integer", + "title": "Size Bytes" + }, + "uploaded_at": { + "type": "string", + "format": "date-time", + "title": "Uploaded At" + }, + "storage_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Storage Path" + } + }, + "type": "object", + "required": [ + "filename", + "content_type", + "size_bytes" + ], + "title": "SourceFile", + "description": "Uploaded source file metadata." + }, + "StartChatResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id" + }, + "agent_name": { + "type": "string", + "title": "Agent Name", + "default": "ai-sre" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "StartChatResponse", + "description": "Response for starting a direct investigation chat." + }, + "StatusCount": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "status", + "count" + ], + "title": "StatusCount", + "description": "Count of incidents by status." + }, + "SubmitFeedbackRequest": { + "properties": { + "rating": { + "type": "integer", + "maximum": 3.0, + "minimum": 1.0, + "title": "Rating", + "description": "Rating: 1 (Bad), 2 (Fine), 3 (Good)" + }, + "comment": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Comment", + "description": "Optional feedback comment" + } + }, + "type": "object", + "required": [ + "rating" + ], + "title": "SubmitFeedbackRequest", + "description": "Request body for submitting session feedback." + }, + "SubmitFeedbackResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "message": { + "type": "string", + "title": "Message" + }, + "feedback_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Feedback Id" + } + }, + "type": "object", + "required": [ + "success", + "message" + ], + "title": "SubmitFeedbackResponse", + "description": "Response after submitting feedback." + }, + "SubscriptionStatus": { + "properties": { + "has_subscription": { + "type": "boolean", + "title": "Has Subscription" + }, + "is_manual": { + "type": "boolean", + "title": "Is Manual", + "default": false + }, + "dollar_limit": { + "type": "number", + "title": "Dollar Limit" + }, + "cycle_day": { + "type": "integer", + "title": "Cycle Day" + }, + "plan_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Plan Name" + }, + "plan_tier": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Plan Tier" + }, + "stripe_price_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Stripe Price Id" + }, + "cancellation": { + "$ref": "#/components/schemas/CancellationInfo" + }, + "plan_change": { + "anyOf": [ + { + "$ref": "#/components/schemas/PlanChangeInfo" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "has_subscription", + "dollar_limit", + "cycle_day", + "cancellation" + ], + "title": "SubscriptionStatus", + "description": "Response model for user subscription status." + }, + "SystemMCP": { + "type": "string", + "enum": [ + "agent_ops", + "incident_ops", + "incident_admin", + "integrations", + "migrations", + "discovery", + "control_plane_openapi", + "cloud_cli", + "k8s_cli", + "raptor_cli", + "catalog_ops", + "schedule_ops", + "custom_functions", + "agent_db", + "a2a_client", + "k8s_discovery", + "artifacts", + "newrelic_cli", + "repo_kb_ops" + ], + "title": "SystemMCP", + "description": "Built-in system MCPs (hardcoded in facets.py)" + }, + "TagCountResponse": { + "properties": { + "tag": { + "type": "string", + "title": "Tag" + }, + "count": { + "type": "integer", + "title": "Count" + } + }, + "type": "object", + "required": [ + "tag", + "count" + ], + "title": "TagCountResponse", + "description": "Tag vocabulary entry for the filter UI." + }, + "TargetType": { + "type": "string", + "enum": [ + "facets_env", + "k8s_integration", + "cloud_integration" + ], + "title": "TargetType", + "description": "Type of infrastructure target.\n\nMatches the k8s_sources pattern from incident ops:\n- facets_env: Facets CP environment (resolved via FacetsIntegration PAT)\n- k8s_integration: Org-level K8s integration (direct kubeconfig)\n- cloud_integration: Org-level cloud integration (AWS/GCP/Azure) \u2014 TODO: not yet used" + }, + "TeamAdminRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email" + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "TeamAdminRequest" + }, + "TeamCreditLimitRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email" + }, + "credit_limit": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Credit Limit" + } + }, + "type": "object", + "required": [ + "email" + ], + "title": "TeamCreditLimitRequest" + }, + "TeamSeatRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email" + }, + "seated": { + "type": "boolean", + "title": "Seated" + } + }, + "type": "object", + "required": [ + "email", + "seated" + ], + "title": "TeamSeatRequest" + }, + "TeamsChannelConfig": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Config ID" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id", + "description": "Internal workspace ID" + }, + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "ChatIntegration ID" + }, + "integration_name": { + "type": "string", + "title": "Integration Name", + "description": "Integration display name" + }, + "provider_workspace_name": { + "type": "string", + "title": "Provider Workspace Name", + "description": "Integration workspace name" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Teams channel ID" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel display name" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name", + "description": "Facets environment to filter incidents" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Creation timestamp" + } + }, + "type": "object", + "required": [ + "id", + "workspace_id", + "integration_id", + "integration_name", + "provider_workspace_name", + "channel_id", + "channel_name", + "created_at" + ], + "title": "TeamsChannelConfig", + "description": "Workspace-level Teams channel configuration for display purposes." + }, + "TeamsConfigureRequest": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id", + "description": "ChatIntegration ID (Teams)" + }, + "channel_id": { + "type": "string", + "title": "Channel Id", + "description": "Teams channel ID (optional, for display and routing)", + "default": "" + }, + "channel_name": { + "type": "string", + "title": "Channel Name", + "description": "Channel display name", + "default": "Teams Channel" + }, + "filter_rules": { + "items": { + "$ref": "#/components/schemas/FilterRule" + }, + "type": "array", + "title": "Filter Rules" + }, + "filter_combinator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "title": "Filter Combinator", + "default": "AND" + } + }, + "type": "object", + "required": [ + "integration_id" + ], + "title": "TeamsConfigureRequest", + "description": "Request to configure Teams channel for a workspace." + }, + "TeamsIntegrationCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "User-given name for this integration" + }, + "app_id": { + "type": "string", + "title": "App Id", + "description": "Azure Application (client) ID from App Registrations in Azure Portal" + }, + "app_secret": { + "type": "string", + "title": "App Secret", + "description": "Azure App Secret (client secret value) from App Registrations \u2192 Certificates & Secrets" + }, + "tenant_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tenant Id", + "description": "Azure AD Tenant ID (required for SingleTenant bots, optional for MultiTenant)" + } + }, + "type": "object", + "required": [ + "name", + "app_id", + "app_secret" + ], + "title": "TeamsIntegrationCreate", + "description": "Schema for creating/connecting an MS Teams integration via Azure Bot Framework." + }, + "TeamsTestResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + } + }, + "type": "object", + "required": [ + "success" + ], + "title": "TeamsTestResponse", + "description": "Response from connection test." + }, + "TitleUpdateRequest": { + "properties": { + "title": { + "type": "string", + "title": "Title" + } + }, + "type": "object", + "required": [ + "title" + ], + "title": "TitleUpdateRequest", + "description": "Request to update session title." + }, + "ToggleNamespaceRequest": { + "properties": { + "included": { + "type": "boolean", + "title": "Included" + } + }, + "type": "object", + "required": [ + "included" + ], + "title": "ToggleNamespaceRequest" + }, + "TokenType": { + "type": "string", + "enum": [ + "pat", + "oauth", + "github_app_user" + ], + "title": "TokenType", + "description": "Token type - PAT or OAuth" + }, + "ToolAnalytics": { + "properties": { + "call_count_30d": { + "type": "integer", + "title": "Call Count 30D", + "default": 0 + }, + "avg_latency_ms": { + "type": "integer", + "title": "Avg Latency Ms", + "default": 0 + }, + "error_rate_pct": { + "type": "number", + "title": "Error Rate Pct", + "default": 0.0 + }, + "last_called_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Called At" + }, + "daily_stats": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Daily Stats" + } + }, + "type": "object", + "title": "ToolAnalytics", + "description": "Analytics summary for a custom tool." + }, + "TopUsersResponse": { + "properties": { + "cycle_key": { + "type": "string", + "title": "Cycle Key" + }, + "users": { + "items": { + "$ref": "#/components/schemas/UserUsageEntry" + }, + "type": "array", + "title": "Users" + }, + "autonomous": { + "items": { + "$ref": "#/components/schemas/AutonomousUsageEntry" + }, + "type": "array", + "title": "Autonomous" + } + }, + "type": "object", + "required": [ + "cycle_key", + "users", + "autonomous" + ], + "title": "TopUsersResponse" + }, + "TriggerContext": { + "properties": { + "thread_messages": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Thread Messages", + "description": "Previous messages in thread/conversation for context" + }, + "metadata": { + "additionalProperties": true, + "type": "object", + "title": "Metadata", + "description": "Source-specific metadata (channel info, incident data, etc.)" + }, + "channel_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel Id", + "description": "Channel/conversation ID" + }, + "channel_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Channel Name", + "description": "Channel name" + }, + "thread_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Thread Id", + "description": "Thread ID (e.g., Slack thread_ts)" + }, + "incident_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Incident Id", + "description": "Incident ID if from incident" + }, + "workspace_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id", + "description": "Internal workspace ID for the agent" + } + }, + "type": "object", + "title": "TriggerContext", + "description": "Context from the trigger source.\n\nContains thread history, metadata, and source-specific information\nthat provides context for the agent." + }, + "TriggerScheduleRequest": { + "properties": { + "extra_instructions": { + "anyOf": [ + { + "type": "string", + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Extra Instructions", + "description": "Optional one-shot note for this run only. Appended to the run prompt as L6 manual instructions. Not persisted as a learning." + } + }, + "type": "object", + "title": "TriggerScheduleRequest" + }, + "TriggerSource": { + "type": "string", + "enum": [ + "slack", + "teams", + "incident", + "api", + "migration", + "catalog", + "agent_schedule", + "a2a", + "webhook" + ], + "title": "TriggerSource", + "description": "Source that triggered the agent run." + }, + "TriggerUser": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "User ID from source (e.g., Slack user ID)" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "Display name" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email", + "description": "Email if available" + }, + "source": { + "$ref": "#/components/schemas/TriggerSource", + "description": "Where this user came from" + }, + "source_user_id": { + "type": "string", + "title": "Source User Id", + "description": "Original user ID from source" + } + }, + "type": "object", + "required": [ + "id", + "source", + "source_user_id" + ], + "title": "TriggerUser", + "description": "User who triggered the agent.\n\nNormalized across different sources." + }, + "UIEventResponse": { + "properties": { + "timestamp": { + "type": "string", + "title": "Timestamp" + }, + "type": { + "type": "string", + "title": "Type" + }, + "payload": { + "additionalProperties": true, + "type": "object", + "title": "Payload" + }, + "agent_label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Label", + "default": "Main" + } + }, + "type": "object", + "required": [ + "timestamp", + "type", + "payload" + ], + "title": "UIEventResponse", + "description": "Serialized UI event for API response." + }, + "UpdateDollarLimitRequest": { + "properties": { + "dollar_limit": { + "type": "number", + "title": "Dollar Limit" + } + }, + "type": "object", + "required": [ + "dollar_limit" + ], + "title": "UpdateDollarLimitRequest" + }, + "UpdateFacetsUserCapRequest": { + "properties": { + "cap_usd": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "Cap Usd" + } + }, + "type": "object", + "required": [ + "cap_usd" + ], + "title": "UpdateFacetsUserCapRequest" + }, + "UpdateHelmApprovalRequest": { + "properties": { + "approval": { + "type": "string", + "title": "Approval" + } + }, + "type": "object", + "required": [ + "approval" + ], + "title": "UpdateHelmApprovalRequest" + }, + "UsageActor": { + "properties": { + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id" + } + }, + "type": "object", + "title": "UsageActor", + "description": "Who triggered the engagement. All fields optional \u2014 a Stop-hook\nextraction always knows the session, sometimes knows the user/agent." + }, + "UsageBulkRequest": { + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "memory", + "skill", + "tool_call", + "catalog_item", + "agent" + ], + "title": "Entity Type", + "description": "One of: memory | skill | tool_call | catalog_item | agent" + }, + "entity_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Entity Ids", + "description": "IDs to fetch counters for. Missing IDs return zeroed rows." + } + }, + "type": "object", + "required": [ + "entity_type" + ], + "title": "UsageBulkRequest" + }, + "UsageSummaryResponse": { + "properties": { + "cycle_key": { + "type": "string", + "title": "Cycle Key" + }, + "total_cost": { + "type": "number", + "title": "Total Cost" + }, + "total_events": { + "type": "integer", + "title": "Total Events" + }, + "by_type": { + "items": { + "$ref": "#/components/schemas/ActorTypeSummary" + }, + "type": "array", + "title": "By Type" + } + }, + "type": "object", + "required": [ + "cycle_key", + "total_cost", + "total_events", + "by_type" + ], + "title": "UsageSummaryResponse" + }, + "UserBanRequest": { + "properties": { + "email": { + "type": "string", + "title": "Email" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + } + }, + "type": "object", + "required": [ + "email", + "organization_id" + ], + "title": "UserBanRequest", + "description": "Request schema for banning/unbanning a user" + }, + "UserBanResponse": { + "properties": { + "success": { + "type": "boolean", + "title": "Success" + }, + "message": { + "type": "string", + "title": "Message" + }, + "user": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserResponse" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": [ + "success", + "message" + ], + "title": "UserBanResponse", + "description": "Response schema for ban/unban operations" + }, + "UserPreferencesResponse": { + "properties": { + "current_permission_mode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions", + "dontAsk" + ], + "title": "Current Permission Mode", + "default": "default" + } + }, + "type": "object", + "title": "UserPreferencesResponse" + }, + "UserResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "email": { + "type": "string", + "title": "Email" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "account_status": { + "$ref": "#/components/schemas/AccountStatus" + }, + "seated": { + "type": "boolean", + "title": "Seated", + "default": false + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "id", + "email", + "organization_id", + "account_status", + "created_at", + "updated_at" + ], + "title": "UserResponse", + "description": "Schema for user API responses (public data only)" + }, + "UserUsageEntry": { + "properties": { + "billing_user_id": { + "type": "string", + "title": "Billing User Id" + }, + "cost": { + "type": "number", + "title": "Cost" + }, + "event_count": { + "type": "integer", + "title": "Event Count" + } + }, + "type": "object", + "required": [ + "billing_user_id", + "cost", + "event_count" + ], + "title": "UserUsageEntry" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "WorkspaceCloudBindingCreate": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Optional region override (uses integration default if not specified)" + } + }, + "type": "object", + "required": [ + "integration_id" + ], + "title": "WorkspaceCloudBindingCreate", + "description": "Request body for binding a cloud integration to a workspace." + }, + "WorkspaceCloudBindingResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name" + }, + "source_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/CloudSourceType" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + }, + "validated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Validated" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "workspace_id", + "integration_id", + "is_active", + "created_at", + "updated_at" + ], + "title": "WorkspaceCloudBindingResponse", + "description": "API response for a workspace cloud binding (with denormalized integration info)." + }, + "WorkspaceCreate": { + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "title": "Name", + "description": "Workspace name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Workspace description" + }, + "agent_context": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ], + "title": "Agent Context", + "description": "Context injected into every investigation prompt" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "WorkspaceCreate", + "description": "Schema for creating a workspace." + }, + "WorkspaceK8sBindingCreate": { + "properties": { + "integration_id": { + "type": "string", + "title": "Integration Id" + } + }, + "type": "object", + "required": [ + "integration_id" + ], + "title": "WorkspaceK8sBindingCreate", + "description": "Request body for binding a K8s source to a workspace." + }, + "WorkspaceK8sBindingResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "workspace_id": { + "type": "string", + "title": "Workspace Id" + }, + "integration_id": { + "type": "string", + "title": "Integration Id" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "integration_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Integration Name" + }, + "cluster_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cluster Name" + }, + "context_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Context Name" + }, + "validated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Validated" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "workspace_id", + "integration_id", + "is_active", + "created_at", + "updated_at" + ], + "title": "WorkspaceK8sBindingResponse", + "description": "API response for a workspace K8s binding (with denormalized source info)." + }, + "WorkspaceListResponse": { + "properties": { + "workspaces": { + "items": { + "$ref": "#/components/schemas/WorkspaceResponse" + }, + "type": "array", + "title": "Workspaces" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "workspaces", + "total" + ], + "title": "WorkspaceListResponse", + "description": "API response for workspace list." + }, + "WorkspaceResponse": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "organization_id": { + "type": "string", + "title": "Organization Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "agent_context": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Context" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "slack_connected": { + "type": "boolean", + "title": "Slack Connected", + "default": false + }, + "slack_channel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Slack Channel" + }, + "mattermost_connected": { + "type": "boolean", + "title": "Mattermost Connected", + "default": false + }, + "mattermost_channel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Mattermost Channel" + }, + "teams_connected": { + "type": "boolean", + "title": "Teams Connected", + "default": false + }, + "teams_channel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Teams Channel" + }, + "webhook_connected": { + "type": "boolean", + "title": "Webhook Connected", + "default": false + }, + "k8s_connected": { + "type": "boolean", + "title": "K8S Connected", + "default": false + }, + "k8s_cluster_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "K8S Cluster Name" + }, + "cloud_connected": { + "type": "boolean", + "title": "Cloud Connected", + "default": false + }, + "cloud_source_names": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Cloud Source Names" + }, + "facets_connected": { + "type": "boolean", + "title": "Facets Connected", + "default": false + }, + "facets_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Url" + }, + "facets_environment_selected": { + "type": "boolean", + "title": "Facets Environment Selected", + "default": false + }, + "facets_environment_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Facets Environment Name" + }, + "facets_environment_has_k8s": { + "type": "boolean", + "title": "Facets Environment Has K8S", + "default": false + }, + "incident_count": { + "type": "integer", + "title": "Incident Count", + "default": 0 + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "created_by_email": { + "type": "string", + "title": "Created By Email" + } + }, + "type": "object", + "required": [ + "id", + "organization_id", + "name", + "is_active", + "created_at", + "updated_at", + "created_by_email" + ], + "title": "WorkspaceResponse", + "description": "API response for a workspace." + }, + "WorkspaceUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "Workspace name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Workspace description" + }, + "agent_context": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ], + "title": "Agent Context", + "description": "Context injected into every investigation prompt" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active", + "description": "Whether workspace is active" + } + }, + "type": "object", + "title": "WorkspaceUpdate", + "description": "Schema for updating a workspace." + }, + "entities__alert_doctor__alert_doctor_models__Severity": { + "type": "string", + "enum": [ + "critical", + "warning", + "info" + ], + "title": "Severity" + }, + "entities__external__facets__facets_integration_models__FacetsProjectsListResponse": { + "properties": { + "projects": { + "items": { + "$ref": "#/components/schemas/FacetsProjectInfo" + }, + "type": "array", + "title": "Projects" + } + }, + "type": "object", + "required": [ + "projects" + ], + "title": "FacetsProjectsListResponse", + "description": "API response for listing Facets projects." + }, + "entities__incident__incident_models__Severity": { + "type": "string", + "enum": [ + "P1", + "P2", + "P3", + "P4", + "P5" + ], + "title": "Severity", + "description": "Incident severity level." + }, + "entities__k8s_migration__k8s_migration_models__HistoryEntry": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Message" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Actor" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "HistoryEntry", + "description": "Audit trail entry for status changes." + }, + "entities__migration__migration_models__FacetsProjectsListResponse": { + "properties": { + "projects": { + "items": { + "$ref": "#/components/schemas/FacetsProjectListItem" + }, + "type": "array", + "title": "Projects" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": [ + "projects", + "total" + ], + "title": "FacetsProjectsListResponse", + "description": "Response for listing Facets projects." + }, + "entities__migration__migration_models__HistoryEntry": { + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + }, + "action": { + "$ref": "#/components/schemas/HistoryAction" + }, + "actor": { + "type": "string", + "title": "Actor", + "description": "'user' or 'agent' or email" + }, + "details": { + "type": "string", + "title": "Details", + "description": "Human-readable description" + }, + "changes": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Changes", + "description": "Before/after for edits" + } + }, + "type": "object", + "required": [ + "action", + "actor", + "details" + ], + "title": "HistoryEntry", + "description": "Audit trail entry." + } + } + } +} \ No newline at end of file