diff --git a/apps/backend/cmd/plugin-fixture/main_test.go b/apps/backend/cmd/plugin-fixture/main_test.go index 2d54b8d38d..decc15c213 100644 --- a/apps/backend/cmd/plugin-fixture/main_test.go +++ b/apps/backend/cmd/plugin-fixture/main_test.go @@ -234,7 +234,7 @@ func TestAuthorizeEntityReference_DeniesUnsupportedPurpose(t *testing.T) { func TestGitCredentialBinding_RevokesAfterAuthenticatedConnectionAction(t *testing.T) { p := &fixturePlugin{dataDir: t.TempDir()} bindingRequest := &pluginsdk.GitCredentialBindingRequest{ - ProviderID: "fixture-source-control", Host: "bitbucket.example.test", Path: "/scm/TEAM/fixture.git", + ProviderID: "fixture-source-control", Host: "bitbucket.example.test", Path: "/scm/TEAM/fixture", } binding, err := p.GetGitCredentialBinding(context.Background(), bindingRequest) @@ -277,7 +277,7 @@ func TestResolveGitCredential_ReturnsTransientFixtureSecret(t *testing.T) { p := &fixturePlugin{dataDir: t.TempDir()} credential, err := p.ResolveGitCredential(context.Background(), &pluginsdk.ResolveGitCredentialRequest{ - ProviderID: "fixture-source-control", Host: "bitbucket.example.test", Path: "/scm/TEAM/fixture.git", + ProviderID: "fixture-source-control", Host: "bitbucket.example.test", Path: "/scm/TEAM/fixture", }) require.NoError(t, err) require.Equal(t, "fixture-user", credential.Username) diff --git a/apps/backend/cmd/plugin-fixture/plugin.go b/apps/backend/cmd/plugin-fixture/plugin.go index 7c365201d5..5d74771e5a 100644 --- a/apps/backend/cmd/plugin-fixture/plugin.go +++ b/apps/backend/cmd/plugin-fixture/plugin.go @@ -32,7 +32,7 @@ const ( revokedPullRequestID = "pull-request-revoked" fixtureProviderID = "fixture-source-control" fixtureCredentialHost = "bitbucket.example.test" - fixtureCredentialPath = "/scm/TEAM/fixture.git" + fixtureCredentialPath = "/scm/TEAM/fixture" connectionStatusAction = "connection-status" searchPurpose = "search" submissionPurpose = "submission" diff --git a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh.go b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh.go index 91f8f8154f..c60b83d43f 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh.go +++ b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh.go @@ -378,7 +378,10 @@ func (r *SSHExecutor) startAndForwardAgentctl( // Keep only the managed broker values in the long-lived remote process. // sshAgentctlLaunchEnv adds the bootstrap credentials required for the // authenticated control handshake without forwarding profile secrets. - env := sshAgentctlLaunchEnv(managedGitHubBrokerEnv(req.Env), nonce) + env := sshAgentctlLaunchEnv( + managedGitCredentialBrokerEnv(sshRemoteContributionEnv(req, agentctlBin)), + nonce, + ) shell := sshShellForRemote(req.Metadata, platform) controlPort, pid, err := startRemoteAgentctl(ctx, client, shell, agentctlBin, taskDir, sessionDir, env, r.logger) if err != nil { @@ -396,7 +399,9 @@ func (r *SSHExecutor) startAndForwardAgentctl( _ = stopRemoteAgentctl(ctx, client, sessionDir, pid) return 0, 0, nil, "", ierr } - instancePort, ierr := createRemoteAgentInstance(ctx, client, controlPort, taskDir, req, authToken, r.logger) + instancePort, ierr := createRemoteAgentInstance( + ctx, client, controlPort, taskDir, agentctlBin, req, authToken, r.logger, + ) if ierr != nil { _ = stopRemoteAgentctl(ctx, client, sessionDir, pid) r.report(req.OnProgress, "Creating agent instance", PrepareStepFailed, ierr.Error()) diff --git a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_connection_test.go b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_connection_test.go index 4f2899fa3c..9ccf81c487 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_connection_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_connection_test.go @@ -11,6 +11,7 @@ import ( "github.com/kandev/kandev/internal/agent/agents" "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/githubauth" "github.com/kandev/kandev/internal/task/models" "golang.org/x/crypto/ssh" ) @@ -493,6 +494,43 @@ func TestSSHRemoteContributionEnvUsesScopedGitCredentialHelper(t *testing.T) { } } +func TestSSHRemoteContributionEnvRewritesPluginCredentialHelper(t *testing.T) { + req := &ExecutorCreateRequest{Env: map[string]string{ + envKeyGitHubCredentialBrokerURL: "https://kandev.example/api/v1/github/credentials/resolve", + envKeyGitHubCredentialLease: "lease", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "credential.https://bitbucket.example.test.helper", + "GIT_CONFIG_VALUE_0": "", + "GIT_CONFIG_KEY_1": "credential.https://bitbucket.example.test.helper", + "GIT_CONFIG_VALUE_1": githubauth.ManagedGitCredentialHelper, + }} + + got := sshRemoteContributionEnv(req, "/home/agent/.kandev/bin/agentctl") + if got["GIT_CONFIG_VALUE_1"] != "!/home/agent/.kandev/bin/agentctl git-credential" { + t.Fatalf("plugin helper = %q, want absolute agentctl helper", got["GIT_CONFIG_VALUE_1"]) + } + if got["GIT_CONFIG_COUNT"] != "2" { + t.Fatalf("GIT_CONFIG_COUNT = %q, want 2", got["GIT_CONFIG_COUNT"]) + } +} + +func TestBuildSSHCreateInstanceRequestRewritesPluginCredentialHelper(t *testing.T) { + req := &ExecutorCreateRequest{Env: map[string]string{ + envKeyGitHubCredentialBrokerURL: "https://kandev.example/api/v1/github/credentials/resolve", + envKeyGitHubCredentialLease: "lease", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "credential.https://bitbucket.example.test.helper", + "GIT_CONFIG_VALUE_0": "", + "GIT_CONFIG_KEY_1": "credential.https://bitbucket.example.test.helper", + "GIT_CONFIG_VALUE_1": githubauth.ManagedGitCredentialHelper, + }} + + got := buildSSHCreateInstanceRequest(req, "/workspace", "/home/agent/.kandev/bin/agentctl") + if got.Env["GIT_CONFIG_VALUE_1"] != "!/home/agent/.kandev/bin/agentctl git-credential" { + t.Fatalf("plugin helper = %q, want absolute agentctl helper", got.Env["GIT_CONFIG_VALUE_1"]) + } +} + func TestSSHRemoteContributionScriptPinsTargetAndSourceIdentity(t *testing.T) { binding := &models.RemoteContribution{ Version: models.RemoteContributionVersion, diff --git a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_control_test.go b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_control_test.go index d77a7018bd..c2c859d632 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_control_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_control_test.go @@ -116,7 +116,7 @@ func TestCreateRemoteAgentInstancePostsTheBuiltRequest(t *testing.T) { } port, err := createRemoteAgentInstance(context.Background(), server.dial(t), - 39429, "/remote/task", req, "bearer-token", newTestLogger()) + 39429, "/remote/task", "/remote/agentctl", req, "bearer-token", newTestLogger()) if err != nil { t.Fatalf("createRemoteAgentInstance: %v", err) } @@ -168,7 +168,7 @@ func TestCreateRemoteAgentInstanceErrors(t *testing.T) { server.forwardTo(strings.TrimPrefix(httpServer.URL, "http://")) _, err := createRemoteAgentInstance(context.Background(), server.dial(t), 39429, - "/remote/task", &ExecutorCreateRequest{InstanceID: "i"}, "", newTestLogger()) + "/remote/task", "/remote/agentctl", &ExecutorCreateRequest{InstanceID: "i"}, "", newTestLogger()) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("error = %v, want %q", err, tc.wantErr) } @@ -194,7 +194,7 @@ func TestBuildSSHCreateInstanceRequestMapsEveryField(t *testing.T) { }, } - got := buildSSHCreateInstanceRequest(req, "/remote/task") + got := buildSSHCreateInstanceRequest(req, "/remote/task", "/remote/agentctl") if got.ID != "instance-9" || got.SessionID != "session-9" || got.TaskID != "task-9" { t.Fatalf("identity = %+v", got) diff --git a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.go b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.go index 0eb24ae37f..e447f2f7a6 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.go +++ b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.go @@ -613,7 +613,11 @@ echo "$AGENTCTL_PID" const sshAgentctlLogTailLines = 25 -func buildSSHCreateInstanceRequest(req *ExecutorCreateRequest, workspacePath string) agentctl.CreateInstanceRequest { +func buildSSHCreateInstanceRequest( + req *ExecutorCreateRequest, + workspacePath string, + agentctlBin string, +) agentctl.CreateInstanceRequest { return agentctl.CreateInstanceRequest{ ID: req.InstanceID, WorkspacePath: workspacePath, @@ -633,7 +637,7 @@ func buildSSHCreateInstanceRequest(req *ExecutorCreateRequest, workspacePath str StripEnv: stripEnvFromReq(req), BaseBranches: getMetadataStringMap(req.Metadata, MetadataKeyBaseBranches), RemoteContributions: req.RemoteContributions, - Env: sshRemoteAgentEnv(req), + Env: sshRemoteContributionEnv(req, agentctlBin), } } @@ -648,11 +652,12 @@ func createRemoteAgentInstance( client *ssh.Client, controlPort int, workspacePath string, + agentctlBin string, req *ExecutorCreateRequest, authToken string, log *logger.Logger, ) (int, error) { - body, err := json.Marshal(buildSSHCreateInstanceRequest(req, workspacePath)) + body, err := json.Marshal(buildSSHCreateInstanceRequest(req, workspacePath, agentctlBin)) if err != nil { return 0, fmt.Errorf("ssh: marshal create-instance: %w", err) } diff --git a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_remote_contribution.go b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_remote_contribution.go index 8571700443..179fc7cb05 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_remote_contribution.go +++ b/apps/backend/internal/agent/runtime/lifecycle/executor_ssh_remote_contribution.go @@ -9,6 +9,7 @@ import ( "golang.org/x/crypto/ssh" + "github.com/kandev/kandev/internal/githubauth" "github.com/kandev/kandev/internal/task/models" ) @@ -67,8 +68,8 @@ func sshRemoteContributionEnv(req *ExecutorCreateRequest, agentctlBin string) ma } count := sshRemoteGitConfigCount(env) managedBroker := hasManagedGitHubBrokerEnv(req.Env) - foundGitHubHelper := rewriteSSHManagedGitHubHelper(env, count, managedBroker, agentctlBin) - if managedBroker && !foundGitHubHelper && agentctlBin != "" { + foundManagedHelper := rewriteSSHManagedGitCredentialHelpers(env, count, managedBroker, agentctlBin) + if managedBroker && !foundManagedHelper && agentctlBin != "" { count = appendSSHGitConfig(env, count, gitHubCredentialHelperConfigKey, "!"+agentctlBin+" git-credential") } if !managedBroker && (env[envKeyGHToken] != "" || env[envKeyGitHubToken] != "") && count < 64 { @@ -88,21 +89,35 @@ func sshRemoteGitConfigCount(env map[string]string) int { return count } -func rewriteSSHManagedGitHubHelper(env map[string]string, count int, managed bool, agentctlBin string) bool { +func rewriteSSHManagedGitCredentialHelpers(env map[string]string, count int, managed bool, agentctlBin string) bool { found := false for index := 0; index < count; index++ { keySuffix := strconv.Itoa(index) - if !strings.EqualFold(env["GIT_CONFIG_KEY_"+keySuffix], gitHubCredentialHelperConfigKey) { + key := strings.ToLower(strings.TrimSpace(env["GIT_CONFIG_KEY_"+keySuffix])) + if !strings.HasPrefix(key, "credential.https://") || !strings.HasSuffix(key, ".helper") { + continue + } + valueKey := "GIT_CONFIG_VALUE_" + keySuffix + if !managed || !isManagedSSHGitCredentialHelper(env[valueKey]) { continue } found = true - if managed && strings.Contains(env["GIT_CONFIG_VALUE_"+keySuffix], "git-credential") { - env["GIT_CONFIG_VALUE_"+keySuffix] = "!" + agentctlBin + " git-credential" - } + env[valueKey] = "!" + agentctlBin + " git-credential" } return found } +func isManagedSSHGitCredentialHelper(value string) bool { + switch value { + case githubauth.ManagedGitCredentialHelper, + githubauth.LegacyShimGitCredentialHelper, + githubauth.LegacyGitCredentialHelper: + return true + default: + return false + } +} + func appendSSHGitConfig(env map[string]string, count int, key, value string) int { if count >= 64 { return count diff --git a/apps/backend/internal/agent/runtime/lifecycle/provider_propagation_test.go b/apps/backend/internal/agent/runtime/lifecycle/provider_propagation_test.go index 71b511f810..f8aff830d1 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/provider_propagation_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/provider_propagation_test.go @@ -25,7 +25,7 @@ func TestAgentctlProviderMappingsPreserveProviderCapabilities(t *testing.T) { container := buildContainerCreateInstanceRequest(ContainerConfig{McpProviders: providers}, "", false, false, false, false, nil) docker := buildReconnectCreateInstanceRequest(req, "previous-execution") sprites := spriteCreateInstanceRequest(req) - ssh := buildSSHCreateInstanceRequest(req, "/workspace") + ssh := buildSSHCreateInstanceRequest(req, "/workspace", "/remote/agentctl") mapped := map[string][]string{ "standalone": standalone.McpProviders, diff --git a/apps/web/e2e/fixtures/ssh-image.ts b/apps/web/e2e/fixtures/ssh-image.ts index c67ed70be2..dd3fff1eac 100644 --- a/apps/web/e2e/fixtures/ssh-image.ts +++ b/apps/web/e2e/fixtures/ssh-image.ts @@ -9,7 +9,7 @@ import path from "node:path"; * * The image carries: * - openssh-server + openssh-sftp-server (the remote target) - * - git + bash + sudo + coreutils (for the SSH executor's prepare scripts) + * - git + bash + curl + sudo + coreutils (for prepare scripts and broker health checks) * - iproute2 + iptables (for fault-injection: drop traffic mid-session) * - a pre-baked `mock-agent` binary at /usr/local/bin/mock-agent so the * agentctl process the SSH executor uploads has something to spawn when @@ -72,6 +72,7 @@ RUN apk add --no-cache \\ openssh-sftp-server \\ git \\ bash \\ + curl \\ sudo \\ shadow \\ coreutils \\ diff --git a/apps/web/e2e/helpers/plugin-git-credentials.ts b/apps/web/e2e/helpers/plugin-git-credentials.ts index d7f12e6a71..49c7f5836c 100644 --- a/apps/web/e2e/helpers/plugin-git-credentials.ts +++ b/apps/web/e2e/helpers/plugin-git-credentials.ts @@ -9,7 +9,7 @@ import { pipeline, type Duplex, type Readable, type Writable } from "node:stream export const fixtureGitProvider = "fixture-source-control"; export const fixtureGitHost = "bitbucket.example.test"; -export const fixtureGitPath = "/scm/TEAM/fixture.git"; +export const fixtureGitPath = "/scm/TEAM/fixture"; export const fixtureGitURL = `https://${fixtureGitHost}${fixtureGitPath}`; export const fixtureGitUser = "fixture-user"; export const fixtureGitSecret = "fixture-credential-secret"; @@ -17,7 +17,7 @@ export const fixtureGitSecret = "fixture-credential-secret"; const fixturePluginID = "kandev-plugin-e2e"; const fixturePluginPackage = path.resolve( __dirname, - "../../../../backend/.build/kandev-plugin-e2e-1.0.0.tar.gz", + "../../../backend/.build/kandev-plugin-e2e-1.0.0.tar.gz", ); type GitHTTPFixture = { @@ -102,11 +102,8 @@ export async function startPluginGitFixture(root: string): Promise [...requestPaths], requestHosts: () => [...requestHosts], @@ -175,33 +172,40 @@ export function assertExactFixtureTransport(fixture: GitHTTPFixture): void { export function runDockerGit( containerID: string, command: string, -): { output: string; status: number } { - const result = spawnSync("docker", ["exec", containerID, "sh", "-lc", command], { - encoding: "utf8", - }); - return { output: `${result.stdout ?? ""}${result.stderr ?? ""}`, status: result.status ?? 1 }; +): Promise<{ output: string; status: number }> { + return runProcess("docker", ["exec", containerID, "sh", "-lc", command]); } export function runRemoteGit( agentctlPID: number, command: string, targetName: string, -): { +): Promise<{ output: string; status: number; -} { - const result = spawnSync( - "docker", - [ - "exec", - targetName, - "sh", - "-lc", - `xargs -0 sh -c 'env "$@" sh -lc "$0"' ${shellQuote(command)} < /proc/${agentctlPID}/environ`, - ], - { encoding: "utf8" }, - ); - return { output: `${result.stdout ?? ""}${result.stderr ?? ""}`, status: result.status ?? 1 }; +}> { + return runProcess("docker", [ + "exec", + "--user", + "kandev", + targetName, + "sh", + "-lc", + `xargs -0 sh -c 'env "$@" sh -lc "$0"' ${shellQuote(command)} < /proc/${agentctlPID}/environ`, + ]); +} + +function runProcess(command: string, args: string[]): Promise<{ output: string; status: number }> { + return new Promise((resolve, reject) => { + const child = spawn(command, args); + const chunks: Buffer[] = []; + child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); + child.stderr.on("data", (chunk: Buffer) => chunks.push(chunk)); + child.once("error", reject); + child.once("close", (status) => { + resolve({ output: Buffer.concat(chunks).toString(), status: status ?? 1 }); + }); + }); } function requestHasFixtureAuth(req: IncomingMessage): boolean { diff --git a/apps/web/e2e/tests/docker/plugin-git-credentials.spec.ts b/apps/web/e2e/tests/docker/plugin-git-credentials.spec.ts index cdbdb2b8c6..edaa5f062b 100644 --- a/apps/web/e2e/tests/docker/plugin-git-credentials.spec.ts +++ b/apps/web/e2e/tests/docker/plugin-git-credentials.spec.ts @@ -46,7 +46,10 @@ test.describe("Bitbucket plugin contract — Docker executor credential leases", config: { image_tag: E2E_IMAGE_TAG }, prepare_script: "", cleanup_script: "", - env_vars: fixture.profileGitEnv, + env_vars: [ + ...fixture.profileGitEnv, + { key: "NO_PROXY", value: new URL(publicBrokerURL).hostname }, + ], }); const repository = await apiClient.createRepository( seedData.workspaceId, @@ -80,18 +83,18 @@ test.describe("Bitbucket plugin contract — Docker executor credential leases", ); const environment = await apiClient.getTaskEnvironment(task.id); expect(environment?.container_id).toBeTruthy(); - const remoteURL = runDockerGit( + const remoteURL = await runDockerGit( environment!.container_id!, "cd /workspace && git remote get-url origin", ); - expect(remoteURL.status).toBe(0); + expect(remoteURL.status, remoteURL.output).toBe(0); expect(remoteURL.output.trim()).toBe(fixtureGitURL); - const push = runDockerGit( + const push = await runDockerGit( environment!.container_id!, "cd /workspace && printf 'pushed\\n' >> README.md && git add README.md && git -c user.name=E2E -c user.email=e2e@test.local commit -m 'credential fixture push' && git push origin HEAD:main", ); - expect(push.status).toBe(0); + expect(push.status, push.output).toBe(0); expect(fixture.pushed()).toBe(true); assertExactFixtureTransport(fixture); @@ -107,7 +110,7 @@ test.describe("Bitbucket plugin contract — Docker executor credential leases", expect(revoke.status).toBe(200); expect(await revoke.text()).not.toContain(fixtureGitSecret); - const afterRevocation = runDockerGit( + const afterRevocation = await runDockerGit( environment!.container_id!, "cd /workspace && git fetch origin", ); diff --git a/apps/web/e2e/tests/ssh/plugin-git-credentials.spec.ts b/apps/web/e2e/tests/ssh/plugin-git-credentials.spec.ts index a43a555611..66197efde4 100644 --- a/apps/web/e2e/tests/ssh/plugin-git-credentials.spec.ts +++ b/apps/web/e2e/tests/ssh/plugin-git-credentials.spec.ts @@ -86,20 +86,20 @@ test.describe("Bitbucket plugin contract — SSH executor credential leases", () ).trim(), ); expect(agentctlPID).toBeGreaterThan(0); - const remoteURL = runRemoteGit( + const remoteURL = await runRemoteGit( agentctlPID, `cd '${row.remote_task_dir}' && git remote get-url origin`, seedData.sshTarget.containerName, ); - expect(remoteURL.status).toBe(0); + expect(remoteURL.status, remoteURL.output).toBe(0); expect(remoteURL.output.trim()).toBe(fixtureGitURL); - const push = runRemoteGit( + const push = await runRemoteGit( agentctlPID, `cd '${row.remote_task_dir}' && printf 'pushed\\n' >> README.md && git add README.md && git -c user.name=E2E -c user.email=e2e@test.local commit -m 'credential fixture push' && git push origin HEAD:main`, seedData.sshTarget.containerName, ); - expect(push.status).toBe(0); + expect(push.status, push.output).toBe(0); expect(fixture.pushed()).toBe(true); assertExactFixtureTransport(fixture); @@ -109,18 +109,15 @@ test.describe("Bitbucket plugin contract — SSH executor credential leases", () ); expect(agentctlLog).not.toContain(fixtureGitSecret); expect(agentctlLog).not.toContain(`${fixtureGitUser}:${fixtureGitSecret}`); - const agentctlEnv = execInContainer(seedData.sshTarget, [ - "sh", - "-c", - `tr '\\0' '\\n' < /proc/${agentctlPID}/environ`, - ]); - expect(agentctlEnv).not.toContain(fixtureGitSecret); - expect(agentctlEnv).not.toContain(`${fixtureGitUser}:${fixtureGitSecret}`); + const agentctlEnv = await runRemoteGit(agentctlPID, "env", seedData.sshTarget.containerName); + expect(agentctlEnv.status, agentctlEnv.output).toBe(0); + expect(agentctlEnv.output).not.toContain(fixtureGitSecret); + expect(agentctlEnv.output).not.toContain(`${fixtureGitUser}:${fixtureGitSecret}`); const revoke = await revokeFixtureConnection(backend.baseUrl, seedData.workspaceId); expect(revoke.status).toBe(200); expect(await revoke.text()).not.toContain(fixtureGitSecret); - const afterRevocation = runRemoteGit( + const afterRevocation = await runRemoteGit( agentctlPID, `cd '${row.remote_task_dir}' && git fetch origin`, seedData.sshTarget.containerName, diff --git a/docs/plans/bitbucket-plugin/plan.md b/docs/plans/bitbucket-plugin/plan.md index 1f849ea798..2227d034b3 100644 --- a/docs/plans/bitbucket-plugin/plan.md +++ b/docs/plans/bitbucket-plugin/plan.md @@ -178,20 +178,25 @@ without provider CSS tokens. Unit, packaged-plugin desktop E2E, mobile contract typecheck, and lint pass. The public authoring reference now includes a code-host hook surface map and focused captures of the shared dashboard, repository picker, Link flow, task status, CI, Review, and composer reference search. -The packaged generic host contract passes on desktop and -mobile. The actual package passes its unconfigured action, disable/re-enable, desktop, -and mobile lifecycle checks; its canonical composer reference now rehydrates the -repository/PR identity and performs live submit-time authorization. Plugin unit, race, -build, and five-platform archive checks pass. Cloud acceptance is complete for the -available disposable target. Task 13 remains in progress until a disposable Data Center -target and a container-reachable HTTPS credential-broker URL are available, the host -changes ship -in a release that can be named by `min_kandev_version`, and the remaining live gates -pass. The initial plugin release intentionally follows the current checksum-verified, -unsigned marketplace contract under +The packaged generic host contract passes on desktop and mobile. The actual package +passes its unconfigured action, disable/re-enable, desktop, and mobile lifecycle checks; +its canonical composer reference rehydrates repository/PR identity and performs live +submit-time authorization. Plugin unit, race, build, and five-platform archive checks +pass. Cloud acceptance is complete for the available disposable target. On 2026-08-14, +the Docker and SSH container specs both passed real HTTPS clone, push, secret +non-disclosure, and connection-generation revocation through the provider-neutral +credential broker. That run also closed the generic remote-helper path used by future +plugin providers on SSH executors. + +Kandev `v0.88.0` and `kandev-plugin-bitbucket` `v0.2.0` are published; the plugin +declares `min_kandev_version: 0.88.0`, and exact build, verify, packaged-host, checksum, +desktop, and mobile gates are green. Task 13 now waits only for the marketplace registry +PR to merge. A configured live Data Center run remains a documented follow-up because +no disposable target was available; Data Center behavior is covered by its separate +adapter fixtures and contract suite. The initial plugin release intentionally follows +the current checksum-verified, unsigned marketplace contract under [ADR-2026-08-01-bitbucket-initial-release-remains-unsigned](../../decisions/2026-08-01-bitbucket-initial-release-remains-unsigned.md); -no signature or cryptographic publisher-provenance claim is made. No plugin tag, -release, or marketplace entry is created before the other gates pass. +no signature or cryptographic publisher-provenance claim is made. ## Risks diff --git a/docs/plans/bitbucket-plugin/task-13-contract-e2e-release-marketplace.md b/docs/plans/bitbucket-plugin/task-13-contract-e2e-release-marketplace.md index ea1fd02d51..873b88b08f 100644 --- a/docs/plans/bitbucket-plugin/task-13-contract-e2e-release-marketplace.md +++ b/docs/plans/bitbucket-plugin/task-13-contract-e2e-release-marketplace.md @@ -3,7 +3,23 @@ id: "13-contract-e2e-release-marketplace" title: "Cross-repository contract E2E, release, and marketplace" status: in_progress wave: 4 -depends_on: ["03-protocol-manifest-actions", "04-frontend-plugin-registry", "05-dynamic-composer-reference-sources", "06-plugin-owned-task-lifecycle", "07-provider-neutral-git-credentials", "08-native-repository-provider", "09-native-link-review-surfaces", "10-cloud-dc-domain-auth", "11-plugin-workflows-watches", "12-plugin-ui-native-registrations", "12b-github-parity-page", "12c-exact-code-host-ui-parity", "12d-host-native-task-link-parity", "12h-native-create-unlink-indicators-saved-queries"] +depends_on: + [ + "03-protocol-manifest-actions", + "04-frontend-plugin-registry", + "05-dynamic-composer-reference-sources", + "06-plugin-owned-task-lifecycle", + "07-provider-neutral-git-credentials", + "08-native-repository-provider", + "09-native-link-review-surfaces", + "10-cloud-dc-domain-auth", + "11-plugin-workflows-watches", + "12-plugin-ui-native-registrations", + "12b-github-parity-page", + "12c-exact-code-host-ui-parity", + "12d-host-native-task-link-parity", + "12h-native-create-unlink-indicators-saved-queries", + ] plan: "plan.md" spec: "../../specs/bitbucket-plugin/spec.md" --- @@ -102,15 +118,23 @@ leakage checks as release gates. an actual build-status transition from successful to in-progress and back, and the disposable marker was restored to successful. - Public docs and the marketplace index baseline validate. -- Container credential specs are present for Docker and SSH, but this workspace lacks - `KANDEV_E2E_CREDENTIAL_BROKER_PUBLIC_BASE_URL`; both tests therefore skip rather than - claiming real HTTPS clone/push evidence. -- The release workflow's independent packaged-plugin runner still fails closed without - `KANDEV_PLUGIN_E2E_URL`. The manifest intentionally omits `min_kandev_version` until - these host contracts ship in a named release and the same package passes against - that released host. +- On 2026-08-14 the exact Docker and SSH container credential specs passed against a + temporary public HTTPS broker endpoint. Both paths cloned and pushed through the + exact provider lease, exposed no credential in process output or logs, and rejected + Git after connection-generation revocation. The run also found and fixed a generic + SSH boundary bug: plugin-provider helpers are now rewritten to the uploaded remote + `agentctl` path for both checkout preparation and the long-lived instance runtime. +- Kandev `v0.88.0` is released. The independently published + [`kandev-plugin-bitbucket` v0.2.0](https://github.com/kdlbs/kandev-plugin-bitbucket/releases/tag/v0.2.0) + declares `min_kandev_version: 0.88.0`; its five platform packages, internal + checksums, build/verify jobs, and packaged desktop/mobile contract are green. +- The final marketplace registry mutation is prepared in the Kandev marketplace PR. + Live Data Center remains an explicit post-publication acceptance follow-up because + no disposable installation was available; Data Center API, auth, context-path, and + capability behavior remains covered by product-specific fixtures and contract tests. - Signing is intentionally deferred by [ADR-2026-08-01-bitbucket-initial-release-remains-unsigned](../../decisions/2026-08-01-bitbucket-initial-release-remains-unsigned.md). The initial package uses mandatory internal checksums, remains visibly unsigned, and makes no cryptographic publisher-provenance claim. Signing is no longer a Task - 13 blocker; the unreleased host version and external live-test gates still are. + 13 blocker. The released host, package, desktop/mobile, and container gates are now + satisfied; merging the marketplace registry PR is the remaining publication step. diff --git a/plugin-registry/plugins.yaml b/plugin-registry/plugins.yaml index b6b1ff9aa8..4dea2fc833 100644 --- a/plugin-registry/plugins.yaml +++ b/plugin-registry/plugins.yaml @@ -35,6 +35,9 @@ plugins: - id: kandev-plugin-slack repo: kdlbs/kandev-plugin-slack categories: [integrations, automation] + - id: kandev-plugin-bitbucket + repo: kdlbs/kandev-plugin-bitbucket + categories: [integrations] - id: kandev-plugin-github-status repo: kdlbs/kandev-plugin-github-status categories: [tools]