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
4 changes: 2 additions & 2 deletions .github/workflows/core-fast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ jobs:
- name: Report core coverage
continue-on-error: true
run: |
go list ./install/integrationctl/agentplugins/... \
(cd install/integrationctl/agentplugins && go list ./... \
| grep -v '/adapters/clientdetect$' \
| xargs go test -count=1 -cover > core-cover.log 2>&1 || true
| xargs go test -count=1 -cover) > core-cover.log 2>&1 || true
(cd cli/plugin-kit-ai && go test -count=1 -cover ./internal/agentpluginscli/... ./cmd/agentplugins/...) >> core-cover.log 2>&1 || true
cat core-cover.log
{
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ jobs:
# incompatible with coverage instrumentation itself.
go test -count=1 -timeout=20m -skip '^TestReleaseCompletionProcessStderr$' -covermode=atomic -coverprofile=coverage/cli.out ./cli/plugin-kit-ai/...
go test -count=1 -timeout=20m -skip '^TestExecutableVersionProbeIsSanitizedAndIsolated$' -covermode=atomic -coverprofile=coverage/integrationctl.out ./install/integrationctl/...
go test -count=1 -timeout=20m -skip '^TestExecutableVersionProbeIsSanitizedAndIsolated$' -covermode=atomic -coverprofile=coverage/agentplugins.out ./install/integrationctl/agentplugins/...
# Nested module: do not use a parent-relative ./agentplugins/... glob.
# That pattern needs go.work; GOWORK=off fails loud instead of covering
# the child. cd keeps this job aligned with CodeQL / release / govulncheck.
agentplugins_cover="${PWD}/coverage/agentplugins.out"
(cd install/integrationctl/agentplugins && go test -count=1 -timeout=20m -skip '^TestExecutableVersionProbeIsSanitizedAndIsolated$' -covermode=atomic -coverprofile="$agentplugins_cover" ./...)
go test -count=1 -timeout=20m -covermode=atomic -coverprofile=coverage/plugininstall.out ./install/plugininstall/...
go test -count=1 -timeout=20m -covermode=atomic -coverprofile=coverage/sdk.out ./sdk/...

Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ lint-baseline-check:
test-core:
# adapters/pathpolicy is outside the agentplugins tree but holds the only
# ports.PathPolicy implementation, so the fast gate has to run it too.
$(CORE_TEST_GIT_ENV) go test -count=1 -timeout=$(CORE_TEST_TIMEOUT) ./install/integrationctl/agentplugins/... ./install/integrationctl/adapters/pathpolicy/...
cd install/integrationctl && $(CORE_TEST_GIT_ENV) go test -count=1 -timeout=$(CORE_TEST_TIMEOUT) ./adapters/pathpolicy/...
cd install/integrationctl/agentplugins && $(CORE_TEST_GIT_ENV) go test -count=1 -timeout=$(CORE_TEST_TIMEOUT) ./...
cd cli/plugin-kit-ai && $(CORE_TEST_GIT_ENV) go test -count=1 -timeout=$(CORE_TEST_TIMEOUT) ./internal/agentpluginscli/... ./cmd/agentplugins/...

test:
Expand All @@ -57,7 +58,7 @@ test-required:
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./...
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./cli/plugin-kit-ai/...
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./install/integrationctl/...
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./install/integrationctl/agentplugins/...
cd install/integrationctl/agentplugins && go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./...
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./install/plugininstall/...
go test -count=1 -timeout=$(REQUIRED_TEST_TIMEOUT) ./sdk/...
cd npm/agentplugins && npm test && npm pack --dry-run --ignore-scripts
Expand Down
10 changes: 6 additions & 4 deletions cli/plugin-kit-ai/cmd/agentplugins/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/directoryv1"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/discoveryv1"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/loader"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/nativeconfig"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/processlock"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/securityscan"
"github.com/777genius/plugin-kit-ai/install/integrationctl/agentplugins/adapters/securityv1"
Expand Down Expand Up @@ -68,9 +69,10 @@ func newAgentpluginsCLIApp(home, dataRoot string, directoryClient *directoryv1.C
mutationLock := processlock.Lock{Path: filepath.Join(dataRoot, "mutation.lock")}
clientRegistry := clientregistry.Default()
paths := pathpolicy.Policy{}
nativeKernel := nativeconfig.New()
stager := newManagedStager(clientRegistry, paths)
planner := clientplanner.Planner{ManagedRoot: filepath.Join(dataRoot, "managed"), Paths: paths, Registry: clientRegistry}
lifecycle := newAgentpluginsLifecycle(dataRoot, v2Store, paths, clientRegistry, stager, runner, planner, directoryManager, mutationLock)
lifecycle := newAgentpluginsLifecycle(dataRoot, v2Store, paths, clientRegistry, stager, runner, planner, directoryManager, mutationLock, nativeKernel)
return assembleAgentpluginsApp(home, dataRoot, v2Store, mutationLock, lifecycle, directoryClient, discoveryClient, securityClient, packageLoader, clientRegistry, planner)
}

Expand All @@ -84,12 +86,12 @@ func newManagedStager(clientRegistry *clients.Registry, paths pathpolicy.Policy)
return stager
}

func newAgentpluginsLifecycle(dataRoot string, v2Store statev2.Store, paths pathpolicy.Policy, clientRegistry *clients.Registry, stager providers.Stager, runner processadapter.OS, planner clientplanner.Planner, directoryManager dirswap.Manager, mutationLock processlock.Lock) usecase.Service {
func newAgentpluginsLifecycle(dataRoot string, v2Store statev2.Store, paths pathpolicy.Policy, clientRegistry *clients.Registry, stager providers.Stager, runner processadapter.OS, planner clientplanner.Planner, directoryManager dirswap.Manager, mutationLock processlock.Lock, nativeKernel nativeconfig.Kernel) usecase.Service {
return usecase.Service{
StateStore: v2Store, Paths: paths, Planner: planner, Targets: planner, Stager: stager,
Activator: providers.Activator{Runner: runner, Registry: clientRegistry},
Activator: providers.Activator{Runner: runner, Registry: clientRegistry, NativeConfig: &nativeKernel},
Lock: mutationLock, Kernel: transaction.Kernel{StateStore: v2Store, Directory: directoryManager},
NativeObserver: providers.NativeIdentityObserver{Stager: stager, Runner: runner, Registry: clientRegistry},
NativeObserver: providers.NativeIdentityObserver{Stager: stager, Runner: runner, Registry: clientRegistry, NativeConfig: &nativeKernel},
PluginData: providers.PluginDataManager{Base: filepath.Join(dataRoot, "plugin-data")},
}
}
Expand Down
11 changes: 6 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ point inward.
| Client adapters | `agentplugins/clients/<id>` | the client contract, `clients/shared`, `domain`, `ports` | yes, `clients-no-upward`, `clients-no-concrete-clients` |
| Adapters | `agentplugins/{adapters,providers,planner}` | the layers above, never `clients/all` | partly, `libraries-take-an-injected-registry` |
| CLI | `agentpluginscli` | the public facades of the layers above, never providers or pathpolicy | yes, `cli-no-core-internals` |
| Composition root | `cmd/agentplugins` | everything, and nothing imports it | yes: it is the only production importer of `clients/all` |
| Composition root | `cmd/agentplugins`, `cmd/plugin-kit-ai` | everything, and nothing imports them | yes: they are the production importers of `clients/all` |

The "Enforced today" column is deliberate: the middle column is the target, and
the left-hand rules are checked by `depguard` in `.golangci.yml`. Domain, ports,
Expand All @@ -71,10 +71,11 @@ adapter into any binary that imports a generic package and would put the registr
outside the composition root's control. `libraries-take-an-injected-registry`
holds the other side of that line: `providers`, `planner` and
`adapters/clientdetect` may not import `clients/all` outside their tests, so the
assembled registry reaches them only as an argument. `cmd/agentplugins` is the
one place that builds it. The CLI receives `Planner`, `Targets` and `Registry`
from that root; it does not construct `planner.Planner{}`. Detection is
request-scoped: `domain.PlanRequest.Detected` is the map `Planner.Plan` reads.
assembled registry reaches them only as an argument. `cmd/agentplugins` and
`cmd/plugin-kit-ai` are the production places that build it. The installer CLI
receives `Planner`, `Targets` and `Registry` from that root; it does not
construct `planner.Planner{}`. Detection is request-scoped:
`domain.PlanRequest.Detected` is the map `Planner.Plan` reads.

`cli-no-core-internals` keeps `agentpluginscli` off `providers` and
`pathpolicy`. The CLI may still import the thin public planner facade
Expand Down
7 changes: 4 additions & 3 deletions docs/adr/0007-client-adapter-contract-and-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ compensation, not a substitute for a module boundary.
## Decision

Client-specific behavior lives in `install/integrationctl/agentplugins/clients/<id>`.
The composition root (`cli/plugin-kit-ai/cmd/agentplugins`) is the only
production package that imports `clients/all`. Generic packages receive a
`clients.Registry` and fail closed when it is nil.
The production composition roots that import `clients/all` are
`cli/plugin-kit-ai/cmd/agentplugins` (installer CLI) and
`cli/plugin-kit-ai/cmd/plugin-kit-ai` (authoring CLI). Generic packages receive
a `clients.Registry` and fail closed when it is nil.

Invariants:

Expand Down
24 changes: 17 additions & 7 deletions docs/plans/installer-core-clean-architecture-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ issues:

Один режим на всё не работает: `new-from-*` фильтрует по изменённым строкам, а `funlen`/`gocyclo`/`file-length-limit` репортят на строке объявления функции/файла — добавление 40 строк в середину старой 200-строчной функции такой фильтр не поймает. Поэтому:

- **Прогон A** — корректность и стиль, только изменённые строки: все линтеры, кроме size/arch. CI: `only-new-issues: true` (на PR — патч через GitHub API; на push — `new-from-rev=<before>`). Локально: `golangci-lint run --new-from-merge-base=$(BASE)`; в checkout нужен `fetch-depth: 0`.
- **Прогон A** — корректность и стиль, только изменённые строки: все линтеры, кроме size/arch. CI и локально: `golangci-lint run --new-from-merge-base=$(LINT_BASE)`. `LINT_BASE` на PR — `origin/<base>` (для stacked part-PR это architecture, для `#288` — `main`). `golangci-lint-action` `only-new-issues` здесь нельзя: reusable `workflow_call` не несёт `github.event.pull_request`, а GitHub Files API отдаёт 406 на diff > 300 файлов и молча падает в полный прогон. В checkout нужен `fetch-depth: 0` и явный `git fetch` базовой ветки.
- **Прогон B** — size/arch-гейт, полные файлы, всегда: `golangci-lint run --enable-only=revive,funlen,gocyclo,gocognit,dupl,depguard`. Легаси-нарушения размера покрыты baseline-исключениями (сгенерированы этим же прогоном в Part 0a); всё остальное строго: новый файл > 500 строк кода, новая функция > 60/40, рост сложности в файле вне baseline, любой запрещённый импорт — красный. `depguard` в full-file режиме — граница держится всегда, а не только на изменённых строках.
- **Baseline shrink-only**: `scripts/check-lint-baseline.sh <base-ref>` извлекает блок `BEGIN/END LEGACY SIZE BASELINE` из `.golangci.yml` в HEAD и из `git show <base-ref>:.golangci.yml`, `comm -13` → любые добавленные `- path:` = ошибка. Файл из скоупа выходит из baseline в той части, которая его режет (критерий приёмки части). В Part 11 блок содержит только файлы вне скоупа (§11).

Expand All @@ -565,21 +565,30 @@ issues:
name: Lint
on:
workflow_call:
inputs:
lint-base: { type: string, default: origin/main }
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module: [".", "cli/plugin-kit-ai", "install/integrationctl"]
include:
- { name: root, module: "" }
- { name: cli/plugin-kit-ai, module: cli/plugin-kit-ai }
- { name: install/integrationctl, module: install/integrationctl }
- { name: install/integrationctl/agentplugins, module: install/integrationctl/agentplugins }
steps:
- uses: actions/checkout@<sha> # v7.0.1, пин по SHA как остальные actions в репо
with: { persist-credentials: false, fetch-depth: 0 }
- name: Fetch lint base
env: { LINT_BASE: ${{ inputs.lint-base }} }
run: git fetch --no-tags origin "${LINT_BASE#origin/}:refs/remotes/origin/${LINT_BASE#origin/}"
- uses: actions/setup-go@<sha> # v7.0.0
with: { go-version: "1.25.13", cache: false }
- name: Lint changed lines (correctness, style)
uses: golangci/golangci-lint-action@<sha> # v9.3.0
with: { version: v2.13.2, working-directory: ${{ matrix.module }}, only-new-issues: true }
with: { version: v2.13.2, working-directory: ${{ matrix.module }}, only-new-issues: false, args: --new-from-merge-base=${{ inputs.lint-base }} }
- name: Size and architecture gate (full files)
uses: golangci/golangci-lint-action@<sha> # v9.3.0
with:
Expand All @@ -588,13 +597,14 @@ jobs:
only-new-issues: false
args: --enable-only=revive,funlen,gocyclo,gocognit,dupl,depguard
- name: Baseline is shrink-only
if: matrix.module == '.'
run: bash scripts/check-lint-baseline.sh "origin/${{ github.base_ref || 'main' }}"
if: matrix.name == 'root'
env: { LINT_BASE: ${{ inputs.lint-base }} }
run: bash scripts/check-lint-baseline.sh "$LINT_BASE"
```

Матрица покрывает модули с кодом ядра (включая nested `install/integrationctl/agentplugins`); `install/plugininstall` и `sdk` из `go.work` линтом не покрываются (не меняются планом), но покрываются `go vet` в `core-fast` (§10).

`ci.yml`: `jobs.lint: uses: ./.github/workflows/lint.yml` параллельно с `test`. Почему отдельный job, а не шаг в `test`: `test` — критический путь 9-27 мин; lint в трёх параллельных матричных job ожидаемо 3-5 мин и не удлиняет путь; при этом он в том же workflow «Required», т.е. входит в required-гейт.
`ci.yml` / `core-fast.yml` передают `lint-base: origin/${{ github.base_ref || 'main' }}` — caller видит `github.base_ref`, called workflow на `workflow_call` его не имеет. Почему отдельный job, а не шаг в `test`: `test` — критический путь 9-27 мин; lint в четырёх параллельных матричных job ожидаемо 3-5 мин и не удлиняет путь; при этом он в том же workflow «Required», т.е. входит в required-гейт.

Версии: golangci-lint v2.13.2 (релиз 2026-08-27) и golangci-lint-action v9.3.0 (2026-06-29) — последние стабильные по GitHub Releases на дату плана (VERIFIED). Локально нужен апгрейд с v1.64.8 (`brew upgrade golangci-lint`): v1 не читает `version: "2"` и не содержит правило `file-length-limit`.

Expand Down Expand Up @@ -979,7 +989,7 @@ Wall 5-7 мин и для `main`. Полезно, но меняет общий C
- Код конкурента AgentBridge (оценки взяты из аудита пользователя).
- Длительность CLI-тестов и root `./...` в CI по отдельности (замерено только локально: `ap/...` = 62.6 с, CLI-пакет = 56 с).
- Точный состав `gocritic disabled-checks`/`gosec excludes` и порог `dupl` — по первому прогону в Part 0a.
- Поведение `golangci-lint-action only-new-issues` для `push`-событий (`new-from-rev=before`) — по документации action; проверить на первом push в базовую ветку.
- Поведение `--new-from-merge-base` на `push` в `main` (`lint-base` = `origin/main` = HEAD): прогон A вырождается в no-op, size/arch всё равно полный. Для architecture-push сравнение с `origin/main` — нужный гейт для `#288`.
- Поведение `depguard` при `relative-path-mode: gitroot` для glob-паттернов `files:` — проверить на негативных тестах Part 0a (§6.7, п.3).
- Аргументы `revive file-length-limit` прочитаны в исходниках `mgechev/revive` (`rule/file_length_limit.go`, `rule/utils.go`, `internal/config/config.go`) на ветке `master`, а не на теге, вшитом в golangci-lint v2.13.2 — теоретически они могли отличаться в момент вендоринга. Негативный тест §6.7 п.3 закрывает это эмпирически на первом прогоне.
- Точное число мест конструирования `Service{}`/`Planner{}`: подсчёты разными способами дают 23/32 (grep по строкам на HEAD) против 23/39 (подсчёт критика). Планируем по верхней границе ≈62; точное число выяснится при первой компиляции после введения fail-fast.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ func (kernel Kernel) Apply(req Request) (Receipt, error) {
return receipts[0], err
}

// RequireFileIO reports that this kernel can Inspect and Apply. A zero Kernel
// has no FileIO and must not be used as a hide-default on skill-only native
// paths that never call ApplyBatch.
func (kernel Kernel) RequireFileIO() error {
if kernel.files == nil {
return fmt.Errorf("native config file IO is required")
}
return nil
}

// ApplyBatch validates and renders related MCP entry mutations into one atomic
// replacement. The batch is all-or-none for cooperating agentplugins writers.
// See conditionalFileIO for the unavoidable portable race with clients that do
// not honor the same locks.
func (kernel Kernel) ApplyBatch(requests []Request) (receipts []Receipt, err error) {
if kernel.files == nil {
return nil, fmt.Errorf("native config file IO is required")
if err := kernel.RequireFileIO(); err != nil {
return nil, err
}
if len(requests) == 0 {
return nil, nil
Expand Down Expand Up @@ -178,8 +188,8 @@ func (kernel Kernel) ApplyBatch(requests []Request) (receipts []Receipt, err err

// Inspect performs a strict read-only ownership check for one native entry.
func (kernel Kernel) Inspect(paths Paths, codec Codec, name string, owned *Receipt) (present bool, exactlyOwned bool, err error) {
if kernel.files == nil {
return false, false, fmt.Errorf("native config file IO is required")
if err := kernel.RequireFileIO(); err != nil {
return false, false, err
}
if strings.TrimSpace(name) == "" {
return false, false, fmt.Errorf("MCP entry name is required")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ import (
"time"
)

func TestRequireFileIORejectsZeroKernel(t *testing.T) {
t.Parallel()
if err := (Kernel{}).RequireFileIO(); err == nil || !strings.Contains(err.Error(), "native config file IO is required") {
t.Fatalf("zero Kernel was not fail-closed: %v", err)
}
if err := New().RequireFileIO(); err != nil {
t.Fatalf("production Kernel rejected FileIO: %v", err)
}
}

func TestMCPServersAddPreservesUnrelatedConfigAndResolvesExplicitPaths(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "client.json")
Expand Down
Loading
Loading