Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude/commands/c1i.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ falls through to whatever the config file names.
(never headers or bodies).

`--debug` and `--max-retries` take effect only on the paths built on the shared
transport (REST, `mcp gateway`, `auth login`, `upgrade`). They are inert on the
`docs` subcommands that fetch — `docs search`, `docs page`, `docs openapi`,
`docs endpoints`, `docs endpoint` — which issue their own HTTP, so no trace
there does not mean no request was sent.
transport (REST, `mcp gateway`, `auth login`, `upgrade`, `docs openapi`,
`docs endpoints`, `docs endpoint`). They are inert on `docs search` and
`docs page`, which issue their own HTTP, so no trace there does not mean no
request was sent.

## Errors & exit codes

Expand Down
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,61 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Upgrading from 0.9.1

With no cached spec, a failed `docs openapi`, `docs endpoints` or
`docs endpoint` now exits 4, 5, 6 or 8 instead of 1, and a 429, a transient
5xx or a network error is retried up to 4 times by default, so an unreachable
host takes several seconds to fail; `--max-retries 0` restores a single
attempt. When the config directory resolves to a relative path (a relative
`HOME`, `%AppData%` or `XDG_CONFIG_HOME`), every command that needs stored
credentials fails instead of using the working directory. With a relative
`HOME`, `~/.c1i.yaml` is ignored.

### Changed

- `golang.org/x/term` moves to 0.47.0 and `golang.org/x/sys` to 0.49.0.

### Fixed

- **`--debug` and `--max-retries` did nothing on `docs openapi`,
`docs endpoints` and `docs endpoint`.** The spec fetch now uses the shared
transport, so it traces, refuses a redirect to a different path or host, and
treats a response over 32 MiB as a failed fetch. With no cache to fall back
on, it retries a 429, a transient 5xx or a network error up to 4 times by
default, and a failed fetch exits 4 for a 404, 5 for a 429, 6 for a 5xx or a
redirect loop, and 8 otherwise, instead of 1. With a cache, it makes one
attempt unless you set `--max-retries` or `C1I_MAX_RETRIES`.

- **An unreadable OpenAPI cache under a day old failed the `docs` OpenAPI
commands** instead of refetching, for example when the cache path was a
directory. They now fetch the spec.

- **The token-file sweep could delete a token another c1i process had just
written**, when that process renamed it over an expired file the sweep had
read. The sweep now removes a file only if it is still the one it read.

- **A relative `HOME`, `%AppData%` or `XDG_CONFIG_HOME` put credentials and
tokens under the working directory.** When the config directory resolves to
a relative path, the credential file fallback now fails with an error naming
it (a browser `auth login` then deletes the credential it just created), and
the token file cache is skipped.

- **`upgrade` no longer runs `go env`.** It ran the go command to find
`GOBIN` and `GOPATH`, which made Go write telemetry counters under your
config dir. It now reads the environment and Go's env file itself. It reads
that file even when Go isn't installed, so a `GOBIN` set there marks a
binary in that dir as a `go install`.

- **With a relative `HOME`, c1i read and wrote `.c1i.yaml` in the working
directory**, so a `.c1i.yaml` planted in a repository could point it, and
any `C1I_CLIENT_ID`/`C1I_CLIENT_SECRET` credentials, at another host. It
now neither reads nor writes the config file then; `auth login` warns that
the URL wasn't saved.

- The README's uninstall steps now say where each install method puts the
binary, and that the Linux config directory follows `XDG_CONFIG_HOME`.

## [0.9.1] - 2026-10-09

### Security
Expand Down
18 changes: 8 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ tracked root file outside its allowlist. Stage explicit paths rather than `-A`.
`--error-format` / `C1I_ERROR_FORMAT` (`text`|`json`), `--dry-run` /
`C1I_DRY_RUN` (preview a mutating request without sending it), `--debug` /
`C1I_DEBUG` (trace HTTP requests to stderr). `--debug` and `--max-retries`
are inert on the fetching `docs` subcommands (`docs search`, `docs page`,
`docs openapi`, `docs endpoints`, `docs endpoint`) — see README for behavior.
are inert on `docs search` and `docs page` — see README for behavior.

### Patterns to follow when adding/changing commands

Expand Down Expand Up @@ -305,15 +304,14 @@ a package, verify each of these against the new code:
retry count the flag can't change. Every caller passes them by hand —
`cmd/client.go:15-16` (the single viper read every REST command inherits via
`newClient`), `internal/client` from its own config, and `cmd/mcp_gateway.go`,
`cmd/auth_login.go`, `cmd/auth_token.go`, `cmd/upgrade.go` from viper. The
deliberate exception is `auth login`'s device-flow polling leg, where
`PollForToken` forces `WithMaxRetries(0)`: the RFC 8628 poll interval is that
call's retry strategy. The accidental one is the fetching `docs` subcommands —
`docs search`, `docs page` (`cmd/docs_search.go`) and `docs openapi`,
`docs endpoints`, `docs endpoint` (`cmd/docs_openapi.go`) — which call
`http.DefaultClient.Do` directly: both flags are inert at those three call
`cmd/auth_login.go`, `cmd/auth_token.go`, `cmd/upgrade.go`,
`cmd/docs_openapi.go` from viper. The deliberate exception is `auth login`'s
device-flow polling leg, where `PollForToken` forces `WithMaxRetries(0)`: the
RFC 8628 poll interval is that call's retry strategy. The accidental one is
`docs search` and `docs page` (`cmd/docs_search.go`), which call
`http.DefaultClient.Do` directly: both flags are inert at those two call
sites, no path or redirect guard applies, and they return bare `fmt.Errorf`
rather than a classifiable error. Don't add a fourth.
rather than a classifiable error. Don't add a third.

When implementing a wire protocol or stream parser (JSON-RPC, SSE, MCP, …), code
and test against the **full input space the spec permits**, not just the shape a
Expand Down
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ c1i docs guide
c1i docs guide register-mcp-server
```

`docs openapi`, `docs endpoints`, and `docs endpoint` use the current public API contract at `https://www.c1.ai/api/openapi.yaml`, including MCP server, tool, and toolset operations. The spec is cached for 24 hours at `~/.c1i/cache/api-openapi.yaml`; fetch failures fall back to that cache, however old, with a one-line warning on stderr naming its age.
`docs openapi`, `docs endpoints`, and `docs endpoint` use the current public API contract at `https://www.c1.ai/api/openapi.yaml`, including MCP server, tool, and toolset operations. The spec is cached for 24 hours at `~/.c1i/cache/api-openapi.yaml`; fetch failures fall back to that cache, however old, with a one-line warning on stderr naming its age. A response over 32 MiB counts as a failed fetch. With a cache to fall back on, a fetch makes one attempt unless you set `--max-retries` or `C1I_MAX_RETRIES`. With no cache, a failed fetch exits `4` for a `404`, `5` for a `429`, `6` for a `5xx` or a redirect loop, and `8` for anything else, such as an unreachable host or a refused redirect.

`docs search` is a semantic search with no relevance threshold: every query returns up to 10 nearest matches, so even a nonsense query comes back with plausible-looking hits. A returned hit is not proof a concept exists, and an unexpected hit is not proof the thing you searched for is absent — read the snippet, or fetch the page with `docs page`, to judge. To check whether an API endpoint exists, use `docs endpoints --filter`, which has a real no-match.

Expand Down Expand Up @@ -1050,13 +1050,13 @@ A chain of allowed redirects that doesn't settle within five hops fails as a
remote error (exit `6`) rather than looping.

This applies to every command built on the shared transport: the REST client,
the MCP gateway, the login handshake, and `upgrade`, so the path and redirect
guards, `--debug` tracing, and `--max-retries` cover all of them, not just REST
commands (`upgrade` maps a refused redirect to exit `8`; see Upgrading).
**None of those four** applies to the `docs` subcommands that fetch —
`docs search`, `docs page`, `docs openapi`, `docs endpoints`, `docs endpoint` —
which call Go's default HTTP client directly: no path or
redirect guard there, and `--debug` and `--max-retries` are both inert.
the MCP gateway, the login handshake, `upgrade`, and `docs openapi`,
`docs endpoints` and `docs endpoint`, so the path and redirect guards, `--debug`
tracing, and `--max-retries` cover all of them, not just REST commands
(`upgrade` and those `docs` commands map a refused redirect to exit `8`).
**None of those four** applies to `docs search` and `docs page`, which call
Go's default HTTP client directly: no path or redirect guard there, and
`--debug` and `--max-retries` are both inert.

One narrower carve-out inside login: the device-flow token poll forces its own
retry count to zero, because RFC 8628's polling interval already *is* that
Expand Down Expand Up @@ -1118,7 +1118,8 @@ Set it via (in order of precedence):

A terminal `c1i auth login` given the URL another way offers to save it
there. If the file isn't a valid YAML mapping, login warns and leaves it
unchanged.
unchanged. With a relative `HOME`, c1i neither reads nor writes it, so a
`.c1i.yaml` in the working directory is never used.

These are equivalent:
- `--url https://mycompany.conductor.one`
Expand Down Expand Up @@ -1307,7 +1308,10 @@ never written to disk — a new one is minted per invocation.

`c1i auth login` writes to the OS keyring when it can and falls back to the
file backend transparently. `c1i auth status` tells you which source served
the active credentials.
the active credentials. If the config directory resolves to a relative path,
from a relative `HOME`, `%AppData%` or `XDG_CONFIG_HOME`, the file fallback
fails with an error and the token cache below skips its file, rather than
writing under the working directory.

### Token cache

Expand Down Expand Up @@ -1392,16 +1396,26 @@ one older than the one you run.

## Uninstalling

Remove the binary the way you installed it: `brew uninstall c1i` for
Homebrew; for `go install`, delete `c1i` from `GOBIN` (set in the environment
or with `go env -w`), or from `$GOPATH/bin` (`~/go/bin` by default) when
`GOBIN` is unset; a downloaded binary is wherever you put it; and
`docker rmi public.ecr.aws/conductorone/c1i:<version>` for the container image.

Removing the binary leaves these behind:

- `~/.c1i/`, the OpenAPI spec cache.
- `~/.c1i.yaml`, the config file.
- `<UserConfigDir>/c1i/`, holding the file-fallback credentials
(`credentials/`) and the access-token file cache (`tokens/`). See
[Credential sources](#credential-sources) for each OS's config directory.
[Credential sources](#credential-sources) for each OS's config directory;
on Linux it is `$XDG_CONFIG_HOME` when that is set.
- OS keyring entries: credentials under `c1i/<host>`, and cached access tokens
under `com.conductorone.c1i.tokens`. Older releases stored a
(including tokens for `C1I_CLIENT_ID`/`C1I_CLIENT_SECRET` credentials) under
`com.conductorone.c1i.tokens`. Older releases stored a
`<name>.conductor.one` tenant's credentials under `c1i/<name>`.
- Any [shell completion](#shell-completion) script you installed, and the
`c1i completion powershell` line if you added it to your PowerShell profile.

c1i can't list keyring entries or keep track of the tenants you've logged in to.
To remove its keyring credentials, run `auth logout` once per tenant before you
Expand Down
41 changes: 25 additions & 16 deletions cmd/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ Get this right first. A wrong tenant returns plausible-looking data with
exit 0.

`c1i` resolves the tenant in this order: `--url`, `C1I_URL`, then `url:` in
`~/.c1i.yaml`. It must be a full host — `mycompany.conductor.one` or
`mycompany.c1eu.ai` (EU) — and `https` is required:
`~/.c1i.yaml` (never read with a relative `HOME`). It must be a full host —
`mycompany.conductor.one` or `mycompany.c1eu.ai` (EU) — and `https` is
required:
a bare `mycompany`, a non-https scheme, and a malformed host (an embedded space
or control character, or a stray scheme like `://host`) are all usage errors
(exit `2`) before any request is sent.
Expand All @@ -42,6 +43,10 @@ subcommands that also take an external server's address — that one is
Credentials resolve in this order: `C1I_CLIENT_ID` + `C1I_CLIENT_SECRET` env
vars (read-only — c1i never writes them), the OS keyring, then a `0600` file
used automatically where no keyring exists (headless Linux, CI, containers).
With a relative `HOME` and no keyring, that file fallback errors rather than
write under the working directory, so every command that needs stored
credentials fails: `auth login`, `auth logout`, and any authenticated command
run without the env vars.
Only the bearer c1i attaches automatically for REST commands is cached and
reused across invocations until it nears expiry, so a run of one-shot commands
does not write a `client_credentials` audit event each time. It uses the OS
Expand Down Expand Up @@ -99,29 +104,33 @@ when the failure came from the API. Still branch on the exit code — the JSON i
for the detail, not the classification.

`--debug` and `--max-retries` cover `mcp gateway`, `auth login` (except its
device-code polling, which never retries) and `upgrade` as well as the REST
commands: the gateway client threads both into its bearer mint and its JSON-RPC
calls. On a `mcp gateway call` that hangs or fails oddly, `--debug` is the
fastest way to see which request stopped.

They do **not** reach the `docs` subcommands that fetch — `docs search`,
`docs page`, `docs openapi`, `docs endpoints`, `docs endpoint`. Those bypass
the shared transport for Go's default HTTP client, so `--debug` prints nothing
and `--max-retries` is ignored. Silent `--debug` output there means the flag
device-code polling, which never retries), `upgrade`, `docs openapi`,
`docs endpoints` and `docs endpoint` as well as the REST commands: the gateway
client threads both into its bearer mint and its JSON-RPC calls. On a
`mcp gateway call` that hangs or fails oddly, `--debug` is the fastest way to
see which request stopped.

They do **not** reach `docs search` or `docs page`. Those bypass the shared
transport for Go's default HTTP client, so `--debug` prints nothing and
`--max-retries` is ignored. Silent `--debug` output there means the flag
never reached that path, NOT that no request was sent — don't read it as
evidence either way when a `docs` command comes back empty.

Nor do those five call the same place, which matters for egress rules and for
why one can fail while another works: `docs openapi`, `docs endpoints` and
`docs endpoint` fetch `https://www.c1.ai/api/openapi.yaml` (cached 24h at
Nor do the fetching `docs` commands call the same place, which matters for
egress rules and for why one can fail while another works: `docs openapi`,
`docs endpoints` and `docs endpoint` fetch
`https://www.c1.ai/api/openapi.yaml` (cached 24h at
`~/.c1i/cache/api-openapi.yaml`, so a run can return rows without sending a
request), while `docs search` and `docs page` call a third party —
`api.mintlify.com` — with a public client-side key. `docs search` is semantic
with no relevance threshold, so it always returns up to 10 plausible hits even
for a nonsense query: never read a result as proof a concept exists, or an
unexpected result as proof it is absent. `docs endpoints --filter` DOES have a
true no-match. If a fetch fails, an expired cache is served with a one-line
`Warning:` on stderr naming its age.
true no-match. If a spec fetch fails, an expired cache is served with a
one-line `Warning:` on stderr naming its age; with no cache, it exits 4 for a
404, 5 for a 429, 6 for a 5xx or a redirect loop, and 8 otherwise. With a cache
to fall back on, the fetch makes one attempt unless you set `--max-retries` or
`C1I_MAX_RETRIES`.

Don't store files in `~/.c1i/cache/`: a spec refresh deletes any file there
that the running version doesn't use.
Expand Down
Loading
Loading