Skip to content

feat: point gitlab-cli at a self-hosted host via GITLAB_HOST - #35

Open
sgraband wants to merge 1 commit into
eclipse-enclave:mainfrom
sgraband:feat/gitlab-cli-self-hosted-host
Open

feat: point gitlab-cli at a self-hosted host via GITLAB_HOST#35
sgraband wants to merge 1 commit into
eclipse-enclave:mainfrom
sgraband:feat/gitlab-cli-self-hosted-host

Conversation

@sgraband

Copy link
Copy Markdown

What it does

Reaching a self-hosted GitLab needed two manual steps: declare GITLAB_HOST so it reached the container, then edit the mixin spec to put the host in serviceAuth.hosts. Both are now unnecessary.

serviceAuth entries gain hostsFromCredential, naming a credential whose resolved value is a host. When it resolves, its value replaces that service's declared hosts. Replacement rather than addition is deliberate: an instance-specific token must not be released to gitlab.com as well.

The override is resolved during auth setup, before the effective policy is resolved and cached, so the host flows into the allow set through the existing ReleaseHosts union and stays reachable. gitlab-cli declares GITLAB_HOST as a non-apiKey credential and wires all three tokens to it.

Closes #4

How to test

Follow-ups

Breaking changes

  • This PR introduces breaking changes and has been coordinated with maintainers.

Review checklist

Reaching a self-hosted GitLab needed two manual steps: declare GITLAB_HOST
so it reached the container, then edit the mixin spec to put the host in
serviceAuth.hosts. Both are now unnecessary.

serviceAuth entries gain hostsFromCredential, naming a credential whose
resolved value is a host. When it resolves, its value replaces that
service's declared hosts. Replacement rather than addition is deliberate:
an instance-specific token must not be released to gitlab.com as well.

The override is resolved during auth setup, before the effective policy is
resolved and cached, so the host flows into the allow set through the
existing ReleaseHosts union and stays reachable. gitlab-cli declares
GITLAB_HOST as a non-apiKey credential and wires all three tokens to it.

Closes eclipse-enclave#4
@sdirix

sdirix commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@EclipseSourceAI

@EclipseSourceAI EclipseSourceAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Autonomous AI review. Generated automatically, so it may contain mistakes. Feel free to ignore any comment you disagree with (noting why helps future reviews).

This review does not approve the change. A human still needs to review it and sign off on the overall architecture and design.

To get an updated review after pushing changes, re-request a review from this account.

Submitted via review-guard-mcp

Adds network.serviceAuth.<id>.hostsFromCredential so a service's HTTP-release hosts can be selected at runtime from another declared credential, and wires gitlab-cli's three tokens to a new gitlab-host (GITLAB_HOST) credential. The plumbing fits the existing shape well: spec validation sits next to the existing serviceAuth cross-reference check, the runtime reuses resolveActiveSecretValue and domainpattern, and releaseHosts clones the secret map so the loaded spec is not mutated. Build, tests, make lint and make generate are all clean.

Two areas deserve a human look. First, replacement narrows the DNS allow set as well as the token release targets, so for tools whose built-in allowlist lacks the gitlab fragment (everything except claude) setting GITLAB_HOST makes gitlab.com unreachable for the whole session, which the stated rationale does not require. Second, the override lands in a mutable Runtime field whose correctness depends on running before the memoized effective-policy resolution, enforced only by a comment.

The rest are smaller: an unredacted credential value in a warning, a wrong per-project secrets path in the feature README, network apply recomputing the bundle without the override, and the host sticking via the persisted env store. Details inline.

host, err = domainpattern.Normalize(host)
}
if err != nil {
logx.Warnf("Secret %s: value %q is not a usable host (%v); keeping declared hosts.", secretID, value, err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prints the credential value verbatim at warn level. If hostsFromCredential ever points at a token (or GITLAB_HOST gets a token value by mistake), the secret lands in the terminal. Everywhere else secret-derived values go through util.RedactSecret, and at debug level:

logx.Debugf("Injected %s from %s (%s)", envVar, secretSource, util.RedactSecret(envValue))

// serviceAuth.hostsFromCredential replacement applied, so a host selected at
// runtime becomes resolvable exactly like a declared one. The loaded spec is
// left untouched.
func (r *Runtime) releaseHosts(secrets map[string]model.SecretConfig) []string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replacement narrows the DNS allow set too, not just the token release targets. Only claude's built-in allowlist pulls in the gitlab fragment (

conf-file=/etc/dnsmasq.allowlists/fragments/gitlab.conf
), so for codex/opencode/theia the feature's release hosts are the only source of gitlab.com and setting GITLAB_HOST makes gitlab.com unresolvable for the whole session. The rationale in the PR description only needs the release hosts narrowed, so releaseHosts could keep the declared hosts while releaseHostsFor uses only the override.

}
// Must precede shouldUseSecretReleases: that call resolves and caches the
// effective policy, which unions the release hosts into the allow set.
m.releaseHostOverrides = resolveReleaseHostOverrides(eligibleSecrets, m.host.Home, layeredSecrets, persistedEnv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invariant is enforced only by this comment, and resolveEffectivePolicy memoizes on the Runtime (

func (r *Runtime) resolveEffectivePolicy() (policy.ResolveResult, error) {
if r.policyResolved {
return r.policyResult, r.policyErr
}
r.policyResolved = true
). It holds today, but a future policy lookup anywhere in prepareMounts (which runs before prepareVolumes) would silently cache an allow set without the override while the token still targets it. Either reset policyResolved here when overrides are non-empty, or fail loudly if the policy is already resolved.


To set it persistently, put it in a secrets file rather than the shell — global
in `~/.local/state/enclave/secrets/global.env`, or per project under
`~/.local/state/enclave/projects/<hash>/`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong path. Per-project secrets live at ~/.local/state/enclave/secrets/projects/<hash>/<tool>.env (

enclave/docs/auth.md

Lines 136 to 138 in 5e80820

| 1 | `~/.local/state/enclave/secrets/global.env` | All agents, all projects |
| 2 | `~/.local/state/enclave/secrets/global/<tool>.env` | Specific agent, all projects |
| 3 | `~/.local/state/enclave/secrets/projects/<hash>/<tool>.env` | Specific agent, specific project |
), and that file is keyed by the tool (e.g. claude.env), not by the feature, which is worth spelling out since this is a mixin.

```

That single env var selects the instance for `glab`, retargets token injection,
and joins the network allowlist — no spec edit or `allow_domains` entry. See

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True at session start, but enclave network apply recomputes the desired bundle from the tool profile only, with no features and no runtime overrides (

func (r EffectiveResolver) loadSpecDomains(tool string) (allowed []string, denied []string, err error) {
profile, err := config.LoadProfile(r.paths, tool)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("load spec domains for %q: %w", tool, err)
}
allowed = append(append([]string(nil), profile.AllowedDomains...), model.ReleaseHosts(profile.Secrets)...)
return allowed, profile.DeniedDomains, nil
), then rewrites it (
applyErr = bundle.WriteConfigBundle(bundle.BundleWriteConfig{
Dir: expectedBundleDir,
Policy: resolved.Effective,
Tool: target.Tool,
})
). So network status reports drift and an apply cuts the self-hosted host out of a running session. The gap already exists for gitlab.com, but a follow-up issue would be good since the override makes it the only host the token can reach.

Comment thread docs/extensions/README.md
- `network.serviceAuth.<service-id>.hostsFromCredential` names another
`credentials.sources` id whose resolved value is a host, for services that
can point at a self-hosted instance. When that credential resolves, its value
**replaces** the declared `hosts` for this service — it is not additive,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It replaces the union of hosts and the serviceDomains-derived hosts, not just the declared hosts: both go into the same list in buildSecrets (

hostsByService := map[string][]string{}
if doc.Network != nil {
for host, id := range doc.Network.ServiceDomains {
hostsByService[id] = append(hostsByService[id], host)
}
for id, auth := range doc.Network.ServiceAuth {
hostsByService[id] = append(hostsByService[id], auth.Hosts...)
}
for id := range hostsByService {
hostsByService[id] = sortDedupeStrings(hostsByService[id])
). Matters for specs like github-cli that express hosts via serviceDomains.

gitlab-job-token: { env: [JOB_TOKEN, CI_JOB_TOKEN] }
# Not a credential: glab reads GITLAB_HOST to pick the instance, and
# hostsFromCredential below retargets token injection at it.
gitlab-host: { env: [GITLAB_HOST], apiKey: false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is a declared credential, the resolved GITLAB_HOST is written to the persisted env store under the default --persist, so later runs without the env var keep the self-hosted override (and keep gitlab.com out of the release hosts). That contradicts "An unset credential keeps the declared hosts" in the docs, so either document how to clear it or skip persistence for non-apiKey host credentials.

continue
}
for _, id := range secretIDs {
overrides[id] = []string{host}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A successful override is completely silent, and the injection debug log redacts the value, so there is no way to see which host was picked. An info line here would help, similar to how ad-hoc allowlisted domains are reported:

logx.Infof("Allowlisted ad-hoc domains: %s", strings.Join(m.run.AllowDomains, ", "))

// otherwise become a bogus allowlist entry.
host, err := domainpattern.NormalizeHost(trimmed)
if err == nil {
host, err = domainpattern.Normalize(host)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domainpattern.Normalize accepts wildcard patterns, so GITLAB_HOST=*.example.com becomes a wildcard release target and allowlist entry. That works against the "names the one instance the token belongs to" rationale, so rejecting * here seems right.

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.

Let gitlab-cli set the GitLab host directly

3 participants