Skip to content

feat(setup): read the setup agent config through the gonfalon relay - #878

Draft
atornsii wants to merge 16 commits into
atorns/setup-agent-stepfrom
atorns/setup-agent-ai-sdk
Draft

atornsii wants to merge 16 commits into
atorns/setup-agent-stepfrom
atorns/setup-agent-ai-sdk

Conversation

@atornsii

@atornsii atornsii commented Oct 19, 2026 •

Copy link
Copy Markdown

This draft adds the Go AI SDK (github.com/launchdarkly/go-ai-sdk) to the CLI. The setup agent reads its instructions and model from the ldcli-setup-agent AI Config, through the SDK relay in gonfalon (launchdarkly/gonfalon#75196 to #75199). No SDK key is on the user's machine.

This PR is stacked on #876, and it also contains the commit of #877, because the SDK needs Go 1.26. Review the last two commits. After #877 merges and #876 picks up main, the upgrade commit leaves this diff.

How it works

  1. When the user is logged in, setup starts a loopback proxy on 127.0.0.1 with a random path. The random path is the per-run secret, so OpenCode or another process cannot use the proxy.
  2. The CLI points all four SDK addresses at the proxy, and it gives the SDK the key ldcli-placeholder.
  3. The proxy adds the user's access token in LD-CLI-Token and forwards each request to <base-uri>/internal/ldcli-setup-agent/sdk/{polling,stream,events,otel}/.... The relay checks the token, adds the real catfood key, and forwards to catfood.
  4. The SDK evaluates the config locally with aiserver.InspectConfig. The CLI turns the variation into the OpenCode model and instructions.

The proxy also solves a gap in the SDK: its trace exporter cannot add a custom header, so without the proxy the trace requests would reach the relay without the token.

If the user is not logged in, if the relay refuses or cannot be reached, or if the config is off or missing, the CLI uses the built-in instructions from #876. Setup does not fail because the config is unavailable.

The config decides the task

When the config loads, its instructions are the agent's system prompt. The first user message gives only the facts from setup (BuildFacts): the SDK, the language, the framework, the package manager, the entry point, whether the SDK is installed, the flag key, and the credential variable. The CLI adds no task of its own, so the instructions decide what the agent does. If the config cannot load or has no instructions, the built-in instructions and the CLI's own task message (BuildPrompt) apply.

Details

  • Every SDK connection option is set in code. The SDK fills an empty option from an environment variable such as LD_BASE_URI, LD_SDK_KEY, or OTEL_EXPORTER_OTLP_ENDPOINT, and the CLI uses LD_BASE_URI for its own API address.
  • The CLI sets LD_ENVIRONMENT_ID for its own process to the client-side ID that the relay sends in the LD-Setup-Agent-Environment-ID response header. If the relay sends no ID, the CLI clears the variable, so a value in the user's shell does not reach the SDK. OpenCode does not get it, because the CLI removes LD_ variables from OpenCode's environment. The relay does not send this header yet. It needs a clientSideId setting in [ldcliSetupAgent]. Until then, the config loads, but runs do not link to the config's Monitoring tab.
  • The CLI makes one polling request through the proxy before the SDK starts. The Go server SDK v7.19.1 treats a 403 on the stream as temporary and retries until its 5-second start timeout ends, so a refusal (for example, the kill switch is off) would otherwise delay every setup run. The check returns the relay's code, for example ldcli_setup_agent_disabled, and it reads the environment ID header.
  • The CLI creates the SDK's LaunchDarkly client with logging off and diagnostic events off, and passes it with InitWithClient. The SDK's own client logs to the terminal, which breaks the wizard's full-screen view.
  • The evaluation context has the kind ldcli-setup, a random key, and these attributes: sdkId, language, framework, packageManager, hasEntryPoint, and sdkInstalled. It has no file paths and no project names.
  • go-ai-sdk is pinned to commit 6152dc9 (v0.0.0-20261016194641-6152dc96d702), because the SDK has no release yet.

Test locally against a local gonfalon

  1. Run the gonfalon stack with sdkKey set under [ldcliSetupAgent] in gonfalon.override.conf.
  2. Turn on enable-ldcli-setup-agent-service in the local flag server: ldcli dev-server add-override --project default --flag enable-ldcli-setup-agent-service --data true.
  3. Turn on "Enable generative AI use cases" for the local account.
  4. ldcli login --base-uri <local gonfalon URL>, so the CLI has a member token.
  5. Run ldcli setup or ldcli setup agent with that base URI.

Cost

  • The binary grows from about 70 MB to 80 MB, because of OpenTelemetry, gRPC, and protobuf.
  • No model provider SDKs and no LangChain packages are in the build.
  • The SDK makes breaking changes before 0.1.0.

Not in this PR

  • Run telemetry (spans, token usage, the outcome metric). It needs a Go OpenCode handler in go-ai-sdk.
  • Managed inference. OpenCode still uses the model key that the user configured for it.
  • A message in the wizard that says why setup used the built-in instructions.

Testing

  • TestAIConfigSource_RealSDKThroughTheProxy starts the real AI SDK through the proxy against a fake relay. The SDK sent only GET .../sdk/stream/all and POST .../sdk/events/bulk, both with the placeholder key and the token, and it read the config.
  • Tests for the proxy: the token header and the path mapping, a 404 without the secret path, streaming without buffering, and an invalid base URI.
  • Tests for a relay refusal and an unreachable relay: both fall back in less than one second.
  • Tests for the options, the context attributes, the provider mapping, the fallbacks, and LD_ENVIRONMENT_ID.
  • go test ./... and pre-commit run --all-files pass.
  • Not tested: a real gonfalon relay and a real catfood config.

🤖 Generated with Claude Code

atornsii and others added 3 commits October 18, 2026 20:06
The Go AI SDK needs Go 1.26 and go-server-sdk v7.19.1. This change
moves the module to Go 1.26 and go-server-sdk from v7.13.4 to v7.19.1.

golangci-lint v1.63.4 cannot read the type data of Go 1.26, so the
pre-commit hook moves to v2.11.4. The new .golangci.yml keeps the v1
rules: staticcheck runs only the SA and S1 checks, and unchecked Close
and print calls are not errors.

The hook now pins Go 1.26.0. Without the pin, pre-commit downloads the
newest Go, and the linter cannot read the type data of a newer release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The setup agent can now get its instructions and model from the
ldcli-setup-agent AI Config. The CLI evaluates the config locally with
the Go AI SDK. The detected facts are the context attributes, so
targeting can give each kind of project its own instructions and model.

The relay does not exist yet, so the source is opt-in for development.
If LDCLI_SETUP_AGENT_SDK_KEY is set, the CLI uses the AI Config. If not,
it uses the built-in configuration. If the SDK cannot start, or the
config is off or missing, the CLI also uses the built-in configuration.

The CLI sets every connection option of the SDK. The SDK fills an empty
option from an environment variable such as LD_BASE_URI, and ldcli uses
that variable for its own API address.

The SDK is pinned to commit 6152dc9, because it has no release yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AI SDK now reads the setup-agent AI Config through the relay in
gonfalon, so no SDK key is on the user's machine. This replaces the
LDCLI_SETUP_AGENT_* variables.

The SDK gets the placeholder key ldcli-placeholder. A loopback proxy in
the CLI adds the user's access token in X-LD-CLI-Token and forwards
each request to /internal/ldcli-setup-agent/sdk under the user's base
URI. The proxy accepts requests only under a random path, so OpenCode
cannot use it. The proxy is also needed because the SDK cannot add a
header to its trace exports.

The CLI makes one polling request before the SDK starts. The Go server
SDK retries a refused stream until its start timeout ends, so without
the check a refusal would delay setup by 5 seconds.

The CLI creates the SDK's LaunchDarkly client itself, with logging off,
because the SDK logs to the terminal and breaks the wizard's view. The
CLI also sets LD_ENVIRONMENT_ID for its own process, so a value in the
user's shell does not reach the SDK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@atornsii atornsii changed the title feat(setup): read the setup agent config with the Go AI SDK feat(setup): read the setup agent config through the gonfalon relay Oct 19, 2026
atornsii and others added 13 commits October 19, 2026 13:59
The CLI no longer hardcodes the client-side ID of the environment that
holds the setup-agent AI Config. The relay sends it in the
X-LD-Setup-Agent-Environment-ID response header, and the CLI reads it
from the check that runs before the SDK starts. The relay holds that
environment's SDK key, so it knows the ID. When the environment
changes, only the gonfalon configuration changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gonfalon removes client request headers that start with X-LD-, because
its private auth headers use that prefix. So the relay never received
X-LD-CLI-Token, and it refused every request with ldcli_token_invalid.

The token header is now LD-CLI-Token, and the environment ID header is
now LD-Setup-Agent-Environment-ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenCode can ask for several tool calls at once. The wizard kept only
one pending request, so each new request replaced the one before it.
The replaced requests never got an answer, and OpenCode waited forever.

The wizard now keeps a queue. It shows the oldest request and how many
more are waiting, and each y or n answers one request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK client now polls through the relay. gonfalon holds back the
relay's streamed response, so the flag stream never started, the SDK
start timed out after 5 seconds, and setup used the built-in
instructions without saying so.

Setup reads the config once per run, so one polling request is all it
needs. Polling also does not depend on a long-lived connection through
the relay and the load balancer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wait-for-app screen printed the SDK key, the client-side ID, and the
mobile key, although each SDK reads only one of them. It now shows only
the credential that the chosen SDK uses. The prompt also names only that
environment variable, so the agent does not have to choose.

The wizard now says "the agent" instead of "OpenCode" in its progress
text and permission questions. Messages that tell the user to install or
upgrade OpenCode keep the name, because the user must act on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent can ask the user to choose with OpenCode's question tool, for
example where the SDK key lives. The wizard did not show these
questions, so the run waited for an answer that never came.

The wizard now shows each question with numbered options. A number picks
an option, and s skips the request, so the agent goes on without
answers. `ldcli setup agent` asks on the terminal the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the instructions of the setup-agent AI Config use a {{variable}},
they describe the task themselves. The CLI fills the variables with the
detected facts and the earlier setup choices, and it sends only a short
start message. So AgentControl controls the whole job without a CLI
release.

Instructions without variables keep the CLI's own task message, so a
config can add guidance without taking over the task.

The variables are sdkId, language, framework, packageManager,
entryPoint, sdkInstalled, flagKey, and credentialEnvVar. The CLI fills
them with aiserver.ParseTemplate, the same function the AI SDK uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the setup-agent AI Config loads, its instructions are the agent's
system prompt, and the first user message gives only the facts from
setup: the SDK, the language, the entry point, the flag, and the
credential variable. The CLI no longer adds its own task, so a task in
the instructions is not overruled by the CLI's flag-only task.

If the config cannot load or has no instructions, the built-in
instructions and the CLI's own task message apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent now runs shell commands in the project without asking. It asks
the user only with the question tool, when it needs information such as
where to store a key. Approving every command made a run slow, and most
commands only read the project.

Some limits stay. Rules deny git commit, git push, git reset, sudo, and
rm -rf on absolute paths. Files outside the project, web access, and
sub-agents stay denied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de handler

The setup agent is now an AI SDK agent. For a logged-in user, setup
calls aiserver.Config("ldcli-setup-agent").Stream with a new OpenCode
handler. The AI SDK resolves the config through the relay, composes the
prompt from it, and calls the handler. The handler sends that prompt to
the OpenCode session and turns the session's events into the SDK's
stream events. The SDK records the run: an opencode.session span, a
chat span per reply with its tokens, an execute_tool span per tool call,
and the tokens, duration, and success events for the config's
Monitoring tab.

The handler implements the AI SDK's handler contract for agent-mode
configs of any provider, so it can move into go-ai-sdk unchanged. The
wizard still gets every OpenCode event, including questions and changed
files. If the SDK cannot resolve the config before the handler starts,
the run uses the built-in instructions and task.

Closing a run that ended now waits up to 5 seconds for the SDK to record
it. Before, the wizard closed the run at once and cut the tracking off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A closed terminal sends SIGHUP, which ended ldcli at once and left the
OpenCode process running. The wizard now quits on SIGHUP the same way
that Bubble Tea quits on SIGINT and SIGTERM, so it stops OpenCode.
`ldcli setup agent` stops OpenCode on SIGINT, SIGTERM, and SIGHUP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent runs shell commands without approvals, and OpenCode's rules
limit only its file tools. In a test run, the agent searched the whole
disk with find, read files in another repo, and read the ldcli access
token with `ldcli config get`.

On macOS, ldcli now starts OpenCode with sandbox-exec and a Seatbelt
profile for each run. The sandbox applies to OpenCode and to every
command that it runs. It denies the home folder, except the project,
OpenCode's own folders, package caches, and toolchains. Other repos,
~/.config/ldcli, and ~/.ssh stay unreadable. System folders and the
network stay open. The profile uses resolved paths, because Seatbelt
matches the real path behind a symlink.

OpenCode's rules also deny `ldcli` and `npx skills` now. The CLI does
the LaunchDarkly work, and skills must not be installed into the repo.

Linux and Windows run without a sandbox for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant