Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ab3658e
docs: record reviewed source transports implementation contract
777genius Oct 23, 2026
391f3b8
feat(sources): define normalized direct source and archive acquisitio…
777genius Oct 23, 2026
ce9b26c
feat(sources): persist URL Git and checksum-bound archive dependency …
777genius Oct 23, 2026
5d50808
feat(sources): acquire native HTTPS and SSH Git without executing pac…
777genius Oct 23, 2026
aa4cf34
fix(sources): enforce native transport policy and reviewed source gra…
777genius Oct 23, 2026
8bc6ade
feat(sources): integrate direct transports with saved package lifecycle
777genius Oct 23, 2026
5e12bd8
fix(sources): repair exact direct revisions and retain operation canc…
777genius Oct 23, 2026
738cd37
fix(sources): isolate native fixtures and satisfy full CI size gates
777genius Oct 23, 2026
cc60206
test(sources): require native SSH and strengthen provenance contracts
777genius Oct 23, 2026
9df2036
docs(sources): record final independent lifecycle review
777genius Oct 23, 2026
07d9298
test(sources): make Unix SSH server fixture prerequisites explicit
777genius Oct 23, 2026
bb6be98
docs(sources): record independent Unix SSH fixture verification
777genius Oct 23, 2026
44dafaf
docs(sources): preserve scoped recovery and validation plan
777genius Oct 24, 2026
c19a15c
docs(sources): record two independent recovery plan approvals
777genius Oct 24, 2026
58cd908
fix(sources): restore escaped paths transitive identities and archive…
777genius Oct 24, 2026
9c6025f
docs(sources): preserve fresh restoration lifecycle and lint results
777genius Oct 24, 2026
9dded00
docs(sources): plan reviewed integration with upstream standalone ski…
777genius Oct 24, 2026
4185343
docs(sources): preserve two final independent restoration code approvals
777genius Oct 24, 2026
9a65dd9
docs(sources): preserve two independent upstream integration plan app…
777genius Oct 24, 2026
7aa99d5
merge(sources): preserve direct package transports alongside upstream…
777genius Oct 24, 2026
571af5a
fix(skills): preserve portable executable metadata during normalization
777genius Oct 24, 2026
9d1a703
feat(skills): install and restore pinned HTTPS publisher sources
777genius Oct 24, 2026
19f1226
fix(sources): preserve literal plus in archive selectors
777genius Oct 24, 2026
4c3667e
fix(sources): complete website Skill lifecycle checks
777genius Oct 24, 2026
cb3e89a
fix(sources): expose website Skills in the production CLI
777genius Oct 24, 2026
0064563
fix(sources): close review notes on website Skill identity and Git en…
777genius Oct 24, 2026
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
68 changes: 68 additions & 0 deletions .github/workflows/source-transports.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Source transports

on:
pull_request:
paths:
- 'install/integrationctl/agentplugins/**'
- 'cli/internal/agentpluginscli/**'
- 'go.work*'
- '**/go.mod'
- '**/go.sum'
- '.github/workflows/source-transports.yml'
push:
branches: [main]
paths:
- 'install/integrationctl/agentplugins/**'
- 'cli/internal/agentpluginscli/**'
- 'go.work*'
- '**/go.mod'
- '**/go.sum'
- '.github/workflows/source-transports.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: source-transports-${{ github.ref }}
cancel-in-progress: true

jobs:
native-ssh:
name: native-ssh-linux
runs-on: ubuntu-24.04
timeout-minutes: 15
env:
UAP_REQUIRE_NATIVE_SSH: '1'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.26.10'
cache: false
- name: Require real OpenSSH tooling
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y openssh-server
sudo install -d -m 0755 /run/sshd
for executable in sshd ssh ssh-keygen git jq; do
command -v "$executable"
done
- name: Verify native Git SSH and host authentication
working-directory: install/integrationctl/agentplugins
shell: bash
run: |
set -euo pipefail
go test -count=1 -timeout=3m -json ./adapters/sourceacquisition -run '^TestNativeSSHGitWithIsolatedKeysAndHostVerification$' | tee native-ssh.json
jq -s -e 'any(.[]; .Action == "pass" and .Test == "TestNativeSSHGitWithIsolatedKeysAndHostVerification") and all(.[]; .Action != "skip")' native-ssh.json
- name: Verify saved CLI SSH URL and SCP lifecycle
working-directory: cli
shell: bash
run: |
set -euo pipefail
go test -count=1 -timeout=3m -json ./internal/agentpluginscli -run '^TestDirectSourceSavedLifecycleRealSSH$' | tee cli-ssh.json
jq -s -e 'any(.[]; .Action == "pass" and .Test == "TestDirectSourceSavedLifecycleRealSSH/ssh-url") and any(.[]; .Action == "pass" and .Test == "TestDirectSourceSavedLifecycleRealSSH/scp") and all(.[]; .Action != "skip")' cli-ssh.json
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ or backup. Transferring the files does not guarantee offline installation.
See [Restore on another machine](docs/UAP_DEPENDENCIES.md#restore-on-another-machine)
for confirmation prompts, supported restore behavior and recovery.

Source-build development: [install raw Agent Skills from GitHub](docs/AGENT_SKILLS_INSTALLER.md),
Source-build development: [install raw Agent Skills from GitHub or compatible HTTPS publishers](docs/AGENT_SKILLS_INSTALLER.md),
including the local browser picker and versioned saved dependencies. This does not
claim availability in an already published installer release.

Expand Down Expand Up @@ -193,6 +193,14 @@ You can also install a local package or a pinned GitHub package without adding
it to the registry. Direct-install examples are collected near the end of this
README.

**SOURCE ONLY / UNRELEASED:** direct source support now includes arbitrary HTTPS
and SSH Git, GitHub/GitLab/Azure browser URLs, and complete HTTPS/local
ZIP/TAR/TAR.GZ/TGZ packages. See [Direct complete package sources](docs/DIRECT_PACKAGE_SOURCES.md)
for syntax, native credentials, immutable archive byte pins and supported limits.
These complete-package `agentplugins add` transports do not import standalone
skill files as plugins. The separate source-build `agentplugins skills add`
workflow above installs raw Agent Skills through deterministic normalization.

Other manifest formats are ignored and cannot override `plugin.json`.

</details>
Expand Down
46 changes: 46 additions & 0 deletions cli/cmd/agentplugins/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
clientregistry "github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/clients/all"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/managedstdio"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/sourcespec"
)

var (
Expand Down Expand Up @@ -295,3 +296,48 @@ func (acquirer lazySourceAcquirer) ResolveGitHubRef(ctx context.Context, reposit
}
return acquirer.acquirer.ResolveGitHubRef(ctx, repository, ref)
}

func (acquirer lazySourceAcquirer) ResolveGitSource(ctx context.Context, source sourcespec.Git) (sourceacquisition.ResolvedGit, error) {
if err := acquirer.prepare(); err != nil {
return sourceacquisition.ResolvedGit{}, err
}
return acquirer.acquirer.ResolveGitSource(ctx, source)
}
func (acquirer lazySourceAcquirer) ResolveGit(ctx context.Context, source sourcespec.Git, selector string, exact bool) (sourceacquisition.RefResolution, error) {
if err := acquirer.prepare(); err != nil {
return sourceacquisition.RefResolution{}, err
}
return acquirer.acquirer.ResolveGit(ctx, source, selector, exact)
}
func (acquirer lazySourceAcquirer) DiscoverGitPackages(ctx context.Context, source sourcespec.Git, commit string) ([]string, error) {
if err := acquirer.prepare(); err != nil {
return nil, err
}
return acquirer.acquirer.DiscoverGitPackages(ctx, source, commit)
}
func (acquirer lazySourceAcquirer) AcquireGit(ctx context.Context, source sourcespec.Git, commit, digest string) (domain.PackageSnapshot, error) {
if err := acquirer.prepare(); err != nil {
return domain.PackageSnapshot{}, err
}
return acquirer.acquirer.AcquireGit(ctx, source, commit, digest)
}
func (acquirer lazySourceAcquirer) AcquireArchive(ctx context.Context, source sourcespec.Archive, archiveSHA, tree string) (domain.PackageSnapshot, error) {
if err := acquirer.prepare(); err != nil {
return domain.PackageSnapshot{}, err
}
return acquirer.acquirer.AcquireArchive(ctx, source, archiveSHA, tree)
}

func (acquirer lazySourceAcquirer) ResolveHTTPSkill(ctx context.Context, endpoint, exactName string) (domain.PackageSnapshot, domain.WebSkillSourceFacts, error) {
if err := acquirer.prepare(); err != nil {
return domain.PackageSnapshot{}, domain.WebSkillSourceFacts{}, err
}
return acquirer.acquirer.ResolveHTTPSkill(ctx, endpoint, exactName)
}

func (acquirer lazySourceAcquirer) AcquireHTTPSkillVerified(ctx context.Context, facts domain.WebSkillSourceFacts) (domain.PackageSnapshot, error) {
if err := acquirer.prepare(); err != nil {
return domain.PackageSnapshot{}, err
}
return acquirer.acquirer.AcquireHTTPSkillVerified(ctx, facts)
}
59 changes: 59 additions & 0 deletions cli/cmd/agentplugins/production_website_skill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"

"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/packagedigest"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
)

// The production composition must expose HTTPS Skill acquisition. Trust is
// granted through the default transport, which the acquirer uses when no
// client is injected, so this exercises the real wrapper and no test double.
func TestProductionSourceAcquirerAcquiresHTTPSkills(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/agent-skills/index.json":
_, _ = fmt.Fprint(w, `{"skills":[{"name":"demo","files":["SKILL.md"]}]}`)
case "/.well-known/agent-skills/demo/SKILL.md":
_, _ = fmt.Fprint(w, "---\nname: demo\ndescription: Test skill\n---\nInert content.\n")
default:
http.NotFound(w, r)
}
}))
defer server.Close()
previous := http.DefaultTransport
http.DefaultTransport = server.Client().Transport
t.Cleanup(func() { http.DefaultTransport = previous })

home := t.TempDir()
app, err := composeAgentpluginsApp(home, filepath.Join(home, "data"))
if err != nil {
t.Fatal(err)
}
acquirer, ok := app.SourceAcquirer.(interface {
ResolveHTTPSkill(context.Context, string, string) (domain.PackageSnapshot, domain.WebSkillSourceFacts, error)
AcquireHTTPSkillVerified(context.Context, domain.WebSkillSourceFacts) (domain.PackageSnapshot, error)
})
if !ok {
t.Fatal("production source acquirer cannot acquire HTTPS Skills")
}
snapshot, facts, err := acquirer.ResolveHTTPSkill(context.Background(), server.URL, "demo")
if err != nil {
t.Fatal(err)
}
defer func() { _ = packagedigest.Remove(snapshot) }()
restored, err := acquirer.AcquireHTTPSkillVerified(context.Background(), facts)
if err != nil {
t.Fatal(err)
}
defer func() { _ = packagedigest.Remove(restored) }()
if restored.TreeDigest != snapshot.TreeDigest {
t.Fatal("locked restore changed the raw tree digest")
}
}
17 changes: 15 additions & 2 deletions cli/internal/agentpluginscli/add_multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -438,7 +439,7 @@ func newAddAcquisitionProof(loaded loadedPackage) (addAcquisitionProof, error) {
ManifestDigest: loaded.envelope.ManifestDigest,
ClosureDigest: groupedAcquisitionClosureDigest(sourceKind, loaded.envelope.Source, loaded.envelope.TreeDigest, loaded.envelope.ManifestDigest),
SourceKind: sourceKind,
Fetched: loaded.envelope.Source.Repository != "",
Fetched: loaded.envelope.Source.Repository != "" || loaded.envelope.Source.SourceKind == "git" || loaded.envelope.Source.SourceKind == "archive" && !filepath.IsAbs(loaded.envelope.Source.SourceURL),
Validated: true,
}, nil
}
Expand All @@ -447,6 +448,9 @@ func acquisitionSourceKind(loaded loadedPackage) string {
if loaded.origin == domain.OriginModeDirectory {
return "directory"
}
if loaded.envelope.Source.SourceKind != "" {
return loaded.envelope.Source.SourceKind
}
if loaded.envelope.Source.Repository != "" {
return "github"
}
Expand All @@ -464,7 +468,9 @@ func acquisitionSourceKind(loaded loadedPackage) string {
// are deliberately excluded because they may contain local, host-specific
// paths. For local acquisitions, source kind plus the two validated package
// identities form the closure; immutable remote acquisitions additionally bind
// repository, subpath, and revision.
// repository, subpath, and revision. New typed direct sources additionally bind
// the normalized endpoint and original archive byte checksum; legacy tuples stay
// byte-identical.
func groupedAcquisitionClosureDigest(sourceKind string, source domain.SourceIdentity, treeDigest, manifestDigest string) string {
hash := sha256.New()
fields := []string{
Expand All @@ -476,6 +482,13 @@ func groupedAcquisitionClosureDigest(sourceKind string, source domain.SourceIden
strings.TrimSpace(treeDigest),
strings.TrimSpace(manifestDigest),
}
if source.SourceKind != "" {
endpoint := source.SourceURL
if filepath.IsAbs(endpoint) {
endpoint = ""
}
fields = append(fields, endpoint, source.ArchiveSHA256)
}
var size [8]byte
for _, field := range fields {
binary.BigEndian.PutUint64(size[:], uint64(len(field)))
Expand Down
4 changes: 2 additions & 2 deletions cli/internal/agentpluginscli/deps_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (resolver dependencyResolver) AcquireCandidate(ctx context.Context, require
return environment.ResolvedPackage{}, err
}
result := environment.ResolvedPackage{Package: pkg, Dependencies: facts}
return result, resolver.finishLoaded(loaded)
return result, resolver.finishLoaded(ctx, loaded, pkg)
}

func dependencyManifestAlias(name string) string {
Expand All @@ -119,7 +119,7 @@ func describePinnedDependency(ctx context.Context, app App, pin environment.Lock
if err != nil {
return environment.ResolvedPackage{}, err
}
input := domain.LoadInput{SnapshotRoot: pkg.Root, TreeDigest: pkg.Metadata.TreeDigest, ExecutableFiles: append([]string{}, pkg.Metadata.ExecutableFiles...), Source: pkg.Metadata.Source}
input := domain.LoadInput{SnapshotRoot: pkg.Root, TreeDigest: pkg.Metadata.TreeDigest, ExecutableFiles: append([]string{}, pkg.Metadata.ExecutableFiles...), Source: identityFromLocked(pin)}
envelope, err := app.loadStandardEnvelope(ctx, input)
if err != nil {
return environment.ResolvedPackage{}, err
Expand Down
21 changes: 16 additions & 5 deletions cli/internal/agentpluginscli/deps_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,28 @@ func runDependencyLock(ctx context.Context, app App, opts *options, updateAlias
hadPrevious := previous.LockVersion != 0
changed := !hadPrevious || !equalDependencyLock(previous, locked)
if allowWrite && changed && !opts.dryRun {
manifestAfter, readErr := os.ReadFile(manifestPath)
if readErr != nil || string(manifestAfter) != string(manifestBefore) {
return dependencyLockResult{}, errors.New("uap.toml changed during lock resolution; rerun deps lock")
}
if err := writeDependencyLock(lockPath, locked); err != nil {
if err := publishResolvedDependencyLock(ctx, app, manifestPath, lockPath, manifestBefore, locked); err != nil {
return dependencyLockResult{}, err
}

}
return dependencyLockResult{Manifest: requirements, Lock: locked, LockPath: lockPath, Changed: changed, DryRun: opts.dryRun}, nil
}

func publishResolvedDependencyLock(ctx context.Context, app App, manifestPath, lockPath string, manifestBefore []byte, locked environment.Lock) error {
manifestAfter, readErr := os.ReadFile(manifestPath)
if readErr != nil || string(manifestAfter) != string(manifestBefore) {
return errors.New("uap.toml changed during lock resolution; rerun deps lock")
}
if err := ensureDependencyStore(ctx, app, locked); err != nil {
return err
}
if err := writeDependencyLock(lockPath, locked); err != nil {
return err
}
return nil
}

func derefLock(lock *environment.Lock) environment.Lock {
if lock == nil {
return environment.Lock{}
Expand Down
10 changes: 10 additions & 0 deletions cli/internal/agentpluginscli/deps_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/sourcespec"
legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports"
)

Expand Down Expand Up @@ -48,6 +49,15 @@ func (s *dependencyTestSource) ResolveGitHubSelector(ctx context.Context, repo,
return s.Acquirer.ResolveGitHubSelector(ctx, repo, selector)
}

func (s *dependencyTestSource) AcquireGit(ctx context.Context, source sourcespec.Git, commit, digest string) (domain.PackageSnapshot, error) {
s.downloads++
return s.Acquirer.AcquireGit(ctx, source, commit, digest)
}
func (s *dependencyTestSource) ResolveGit(ctx context.Context, source sourcespec.Git, selector string, exact bool) (sourceacquisition.RefResolution, error) {
s.lookups++
return s.Acquirer.ResolveGit(ctx, source, selector, exact)
}

// Real Git acquisition, standard loader, digest store, public SDK transaction,
// Codex projection and CLI commands run against exclusively disposable roots.
func TestDependencyEnvironmentCLIEndToEnd(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions cli/internal/agentpluginscli/deps_fresh_restore_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/domain"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/environment"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/installer"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/sourcespec"
legacyports "github.com/777genius/plugin-kit-ai/install/integrationctl/ports"
)

Expand All @@ -40,6 +41,15 @@ func (source *freshRestoreSource) AcquireGitHubVerified(ctx context.Context, rep
return snapshot, err
}

func (source *freshRestoreSource) AcquireGit(ctx context.Context, git sourcespec.Git, commit, digest string) (domain.PackageSnapshot, error) {
source.verified = append(source.verified, freshRestoreFetch{git.Repository, commit, git.Path, digest})
snapshot, err := source.dependencyTestSource.AcquireGit(ctx, git, commit, digest)
if err == nil {
source.snapshots = append(source.snapshots, snapshot.Root)
}
return snapshot, err
}

type freshRestoreRunner struct {
*dependencyFixtureRunner
calls int
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/agentpluginscli/deps_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestDependencyLockedShortNameDoesNotRecheckInstallEligibility(t *testing.T)
_ = seed.cleanup()
t.Fatal("fixture source does not match the recorded release digests")
}
if err := (dependencyResolver{app: fixture.cli.app}).finishLoaded(seed); err != nil {
if err := (dependencyResolver{app: fixture.cli.app}).finishLoaded(context.Background(), seed); err != nil {
t.Fatal(err)
}
requirements.Dependencies["docs"].Registry.Distribution = distribution.ProductID
Expand Down
Loading
Loading