Skip to content

feat(skills): private Git skill sources via egress gateway credentials - #3099

Draft
acherifi wants to merge 4 commits into
kagent-dev:mainfrom
acherifi:feat/private-git-skill-sources
Draft

acherifi wants to merge 4 commits into
kagent-dev:mainfrom
acherifi:feat/private-git-skill-sources

Conversation

@acherifi

@acherifi acherifi commented Oct 8, 2026 •

Copy link
Copy Markdown

Problem

Standalone skills need private HTTPS Git access without exposing credentials to the runtime (#2815). Startup networking is unavailable on some Substrate deployments (#2604). A credential field alone does not solve that constraint.

Changes

Credential references and authority

  • skills[].source.git.authorizationFrom references a Secret key in the AgentTemplate namespace. The key contains the full Authorization header value.
  • The Secret owner must authorize one exact HTTPS origin with kagent.dev/git-credential-origin, for example https://github.com. The compiler compares scheme, DNS name, and port. Wildcards, IP addresses, URL credentials, query strings, and fragments are rejected.
  • Missing Secrets, missing or empty keys, and incorrect origin grants produce ResolvedRefs=False. Secret changes trigger recompilation for root and Shared child templates.
  • This separates Secret authority from template editing. A template author cannot pair an arbitrary Secret with an attacker-controlled origin.
  • Credentials use current main's full-origin bindings. Conflicting credentials for the same origin and header remain rejected.
  • The runtime receives only gateway_authorization: true, not the Secret value or its reference. The typed Go ADK protobuf contract preserves this flag.

Example Secret metadata:

metadata:
  name: skills-auth
  annotations:
    kagent.dev/git-credential-origin: https://github.com

The Secret's authorization key holds the full header value. The template uses authorizationFrom: {name: skills-auth, key: authorization}.

Git transport

  • Git sends Authorization: kagent-credential-injected through per-command http.<repository URL>.extraHeader configuration.
  • The gateway replaces the header only when a matching request already carries it.
  • Authenticated fetches reject redirects. Git prompts are disabled. Cancellation reaches the Git process, and failed downloads are removed.
  • No authorization configuration is persisted in .git/config.

Startup and task-time loading

  • Go ADK, Claude, and Codex attempt standalone Git and S3 fetches at startup, with a 30-second limit.
  • A startup failure logs a warning and permits preparation to continue. Each task retries missing skills before invoking the agent.
  • A persistent failure fails the task with skills_unavailable: …. Successful installations remain reusable while their files exist.
  • Installation uses a staging directory on the destination filesystem. Skill discovery cannot see a partially installed manifest.
  • OCI sources retain feat: mount OCI skills through Substrate image volumes #3203's worker-mounted image delivery and eager validation. Plugins remain eager.
  • Current main is merged. Obsolete CloneGit and runGit helpers remain removed. CRDs and protobuf outputs are regenerated.

Security and limits

The gateway binding covers an origin, not a repository path. Tools can use its authority by sending an Authorization header to that origin. Use read-only, repository-scoped tokens and separate Secret-write permissions from template-write permissions.

The annotation is a compilation-time grant. Removing it blocks new successful compilations; it does not revoke existing pinned Actors. Immediate revocation requires the platform's credential and egress controls.

Do not grant the shared ate-golden identity unrestricted namespace Secret access. Startup fetching succeeds only when networking, egress policy, and credential-provider grants permit it. This PR does not change those platform grants.

Credentials never enter the runtime. Private skill files can enter golden or session snapshots when fetching succeeds. Snapshot access controls must protect that content.

authorizationFrom remains unsupported on plugins. SSH authentication and token exchange are unsupported. Python ADK does not yet consume the agent_plugins runtime contract.

Related work

Verification

  • Passed affected Go packages with -race: skillsinit, agentplugins, translator, controller, v1alpha3 CRD validation, Go ADK config/agent/A2A, and Claude/Codex harnesses.
  • Passed all 65 Python configuration contract tests.
  • Passed make -C go lint with zero issues.
  • Regenerated CRDs and protobuf outputs with make controller-manifests proto-generate.

Regression coverage includes origin grants, missing Secret keys, root/shared-child Secret reconciliation, redirect rejection, startup recovery, and runtime failure handling.

A standalone smoke exercised the actual materializer with real Git and git http-backend. A local proxy simulated replace-only credential injection. It verified unavailable startup, task recovery, pinned content, executable skill files, durable reuse, successful startup, unauthorized fetch rejection, and redirect blocking.

A live private-repository fetch through Substrate was not run for this update. The local proxy smoke does not replace that verification. The PR remains draft.

Add an optional `authorizationFrom` Secret key reference to
AgentTemplate `skills[].source.git`. The compiler binds the key to the
repository hostname as a Substrate egress gateway credential and the
runtime sends a placeholder `Authorization` header for the gateway to
replace, so the token never enters the Actor, its configuration, or a
snapshot.

A gateway credential only applies once an Actor has egress, which
Substrate activates after readiness and never grants the golden Actor
(kagent-dev#2604). Runtimes therefore fetch standalone skills before the first
task of a session Actor instead of at startup: the Go ADK, Claude and
Codex runtimes call Materialization.EnsureSkills per task, and a fetch
failure fails that task with a `skills_unavailable` message instead of
crashing the golden Actor. Plugins keep their startup fetch and reject
`authorizationFrom`.

Refs kagent-dev#2604, kagent-dev#2815

Signed-off-by: acherifi <ali.cherifi@homagames.com>
@github-actions github-actions Bot added the enhancement New feature or request label Oct 8, 2026
@QuentinBisson

Copy link
Copy Markdown
Contributor

We hit the same failure on a Substrate line with replace-only replace_headers: a private git skill failed with could not read Username, because git sends no Authorization before a challenge. I checked that git sends a per-command http.extraHeader on its very first info/refs request, so the placeholder works through the gateway without a 401 round-trip.

A few notes:

  1. Nit: http.<url>.extraHeader instead of http.extraHeader keeps the placeholder on the source's own URL (it can be passed per invocation through GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0 or -c).
  2. In Limits: with replace-only semantics the credential goes on requests to that host that carry an Authorization header, not on every request. A tool can still set one, so the read-only, repository-scoped token advice stands.
  3. Fetching at the first task is the right call while golden actors have no egress ([BUG] Git, OCI, and S3 Agent Plugin resources cannot be materialized while creating Substrate golden Actors #2604). It does put one fetch per repository in every session's first task, so host availability and rate limits now apply per session. Once golden-time egress exists (Configure preview golden egress policies on actor templates agent-substrate/substrate#2159 with Trigger CI workflows on all release/** branches #2117), moving the fetch back to golden boot would fetch once per revision, keep content in the golden snapshot, and let plugins use the same field. Glad to take that as a follow-up rather than grow this PR.

I'll close #2899 in favour of this one. The full-header Secret is the better shape.

@pood1e

pood1e commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Tested this PR on a three-node K3s cluster with kagent v1.0.0-alpha9 (46fdd3d, this PR's base), Substrate 0.4.0-alpha1 and the microvm sandbox class (Kata Containers 4.1.0, Cloud Hypervisor v53.0). The Codex and Claude harness images were built from 46fdd3d plus this PR with upstream's Dockerfiles and default CLI versions; the controller is the stock alpha9 image.

Before (stock alpha9 harnesses). An AgentTemplate that selected skills through plugins[] from a public Git repository at a pinned commit never became ready: Ready=False, ActorTemplateFailed, GoldenActorCrashed. The worker logged the golden Actor's wakeup probe failing for 30 s (connection refused on :8081), and the egress gateway recorded no request, which matches #2604.

With this PR. The same templates use standalone skills[] (source.git with url and commit, and path: skills/<name>):

  • The golden snapshot became ready (ActorTemplate golden snapshot is ready) for both the Codex and the Claude agent.
  • On the first task, both runtimes fetched the skill through the egress gateway. Codex read it from /data/codex/skills/<name>/SKILL.md, and Claude loaded it with the Skill tool from /data/generated/claude/.claude/skills/<name>/SKILL.md.
  • The unit tests of core/pkg/agentplugins, core/internal/skillsinit and harness/... pass on the PR head.

Not covered: authorizationFrom, because our skill repository is public. plugins[] still fails at golden creation, as the PR description says.

We now run these patched harness images in our development environment until a release includes this fix. Thanks for working on it!

@QuentinBisson

Copy link
Copy Markdown
Contributor

Follow-up to my earlier comment, after pood1e's run and the conflict with #3144.

The credential half is proven on a Substrate line where golden actors already have egress: a private GitHub skill fetched with the placeholder through the real gateway (200 on info/refs and git-upload-pack), and the control without the placeholder failing with could not read Username. authorizationFrom differs from that proof only in the compiler binding, which the unit tests here cover. So I would not block on an e2e for it.

On the deferred fetch: it is the right workaround for #2604 on Substrate today, but skills are immutable and the end state is one fetch per revision with the content in the golden snapshot, which is what agent-substrate/substrate#2159 (goldenEgressPolicy, with agent-substrate/substrate#2117 for boot-time egress) is heading for. As written, this PR hard-wires "never at startup" into every runtime, so moving the fetch back later is a second runtime change, and non-Substrate deployments lose the startup fetch they have today. Suggestion: keep EnsureSkills as is, call it once best-effort at startup and log the failure (the golden fails fast today since atunnel closes the connection; it succeeds once the golden has a policy and a credential grant), and keep the per-task call as the fallback. Same API, same contract flag, same failure reporting; it converges on golden-time fetch without further changes. The one thing the deferred fetch gets right that golden-time fetch does not have yet is credential authority: goldens share the ate-golden atespace, so a Secret grant there is cluster-wide until #2159 gives goldens a scoped identity. Worth stating in the body either way.

The branch needs a rebase: #3144 removed CloneGit and runGit from skillsinit/git.go, and the Agents CRD YAMLs are regenerated. The earlier nit stands: http.<url>.extraHeader keeps the placeholder off redirect targets.

}
// The runtime sends a placeholder Authorization header that the
// gateway replaces with the Secret value.
if err := bind(git.URL, "authorization", "", agent.Template.Namespace, git.AuthorizationFrom.Name, git.AuthorizationFrom.Key); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no ResolvedRefs failure when this secret or key is missing

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in dafd341. The compiler now resolves the same-namespace Secret through KRT before emitting credentials. Missing Secrets and missing or empty keys produce ResolvedRefs=False with ReferenceResolutionFailed. Secret changes trigger recompilation for root and Shared child templates. The new controller test covers missing Secret → missing key → missing origin grant → success → grant removal without editing the Agent. Compiler and controller tests pass with -race.

if agent.Template != nil {
for _, skill := range agent.Template.Spec.Skills {
git := skill.Source.Git
if git == nil || git.AuthorizationFrom == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what stops pointing url at an attacker host with any secret?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in dafd341. A Secret owner must grant one exact HTTPS origin through kagent.dev/git-credential-origin. Template authors cannot change that grant through an AgentTemplate. The compiler checks scheme, DNS name, and port before emitting a binding; missing grants, different hosts/ports, wildcards, and malformed origins fail compilation. Git also scopes its placeholder to http.<repository URL>.extraHeader and rejects authenticated redirects. Tests cover attacker hosts, invalid grants, and redirects. The gateway authority remains origin-wide, so the docs require read-only repository-scoped tokens and separate Secret-write permissions. Grant removal prevents new compilations; it does not revoke existing pinned Actors.

Signed-off-by: acherifi <ali.cherifi@homagames.com>
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Oct 19, 2026
@acherifi

Copy link
Copy Markdown
Author

Updated in dafd341 and merged current main (7a0a4fd0). The conflicts are resolved, including #3203's OCI mounts and the typed Go ADK configuration.

@QuentinBisson: Git now uses per-command http.<repository URL>.extraHeader; authenticated redirects are rejected. The documentation states that the gateway replaces only headers present in the request. Standalone Git/S3 skills get a bounded best-effort startup fetch, with a warning on failure and task-time retries. Mounted OCI skills remain eager. The shared ate-golden credential-authority caveat is documented; this change grants it no extra Secret access. Private content can enter snapshots when startup fetching succeeds.

@moezdil: both review comments have code changes and regression coverage. Missing Secrets/keys fail ResolvedRefs, and the Secret owner must grant an exact HTTPS origin before a template can use its credential.

Verification:

  • Affected Go packages passed with -race, including API envtest, controller Secret-change reconciliation, all translator packages, Go ADK, and Claude/Codex harnesses.
  • All 65 Python configuration contract tests passed.
  • make -C go lint passed with zero issues.
  • CRDs and protobuf outputs were regenerated.
  • A smoke run used the actual materializer, real Git, and git http-backend behind a local replace-only credential proxy. Startup fallback, task recovery, pinned content, executable skill files, durable reuse, authorized startup, unauthorized failure, and redirect blocking passed.

The PR body now reflects this behavior. This update did not run a private Git fetch through a live Substrate gateway, so I have left the PR as draft. No cluster resources were changed.

Signed-off-by: acherifi <ali.cherifi@homagames.com>
@acherifi

Copy link
Copy Markdown
Author

Merged current main (2261632c) in 6c5b48e9 and resolved the conflicts.

  • Preserved deferred skill discovery alongside the upstream built-in tools configuration.
  • Regenerated protobuf outputs from the merged schema.
  • Updated the controller test to the new go.ate.dev/substrate import path.

Verification passed: affected Go packages with -race, 65 Python contract tests, and lint with zero issues. A runtime smoke confirmed late skill discovery, unique built-in tools, disabled Bash, and preservation of the gateway authorization flag. No cluster resources were changed.

Signed-off-by: acherifi <ali.cherifi@homagames.com>
@acherifi

Copy link
Copy Markdown
Author

Updated the branch from main (b672187f) in 2d39a97c and resolved the conflicts.

  • Kept both documentation sections: upstream's gateway token passthrough and this PR's private Git skill sources.
  • Kept both test imports: egress.Credential bindings and egresspolicy.GatewayTarget passthrough targets.

Verification passed: affected Go packages with -race, 65 Python contract tests, and lint with zero issues. No cluster resources were changed.

QuentinBisson added a commit to giantswarm/kagent-upstream that referenced this pull request Oct 24, 2026
)

kagent-dev#3340 files the Harness conditions (Ready from the WorkerPool,
Booted from the golden boots); the status merge patch files on its own later.
The git fetch retry rows wait for kagent-dev#3099, which rewrites
CloneGitCommit, and carry the notes for the re-cut; the durable copy is
superseded by kagent-dev#3099's task-time skill install. The SKILL.md frontmatter row
points at google/adk-go#1957 and kagent-dev#1958.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants