Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

856 lines
30 KiB
Go

package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"strings"
"sync"
"time"
"unicode/utf8"
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
)
const credentialBrokerInitialEventTimeout = 30 * time.Second
const credentialBrokerContinuationWait = time.Second
type managedCLILaunch struct {
arguments []string
artifactTrees []agentruntime.ArtifactTreeIdentity
cwd string
executable connectorruntime.ConnectorExecutable
language string
stateDir string
timeout time.Duration
}
type credentialBrokerCLILaunch struct {
Executable string `json:"executable"`
Arguments []string `json:"arguments"`
CWD string `json:"cwd"`
}
type managedCredentialBrokerLaunch struct {
entrypoint string
timeout time.Duration
allowedHosts map[string]struct{}
cliLaunch credentialBrokerCLILaunch
executable connectorruntime.ConnectorExecutable
language string
cwd string
artifactTrees []agentruntime.ArtifactTreeIdentity
stateDir string
}
type managedCredentialAuthorizationHost interface {
authorizationRoute(context.Context, market.OperationScope, market.Connector) (*connectorRoute, error)
startCredentialBroker(context.Context, *connectorRoute, credentialBrokerRequest) (agentruntime.ProcessConnection, uint64, error)
observeAuthorization(*connectorRoute, market.AuthorizationState)
releaseAuthorizationRoute(*connectorRoute)
}
// managedCredentialAuthorizationProvider owns the host-side lifecycle of a
// connector-provided credential broker. Provider-specific commands, response
// parsing, and secret handoffs stay inside the verified connector adapter.
type managedCredentialAuthorizationProvider struct {
host managedCredentialAuthorizationHost
mu sync.Mutex
sessions map[string]*credentialBrokerSession
activeByRoute map[string]string
}
type credentialBrokerRequest struct {
Protocol string `json:"protocol"`
Operation string `json:"operation"`
}
type credentialBrokerEvent struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
type credentialBrokerSession struct {
operationID string
route *connectorRoute
connection agentruntime.ProcessConnection
cancel context.CancelFunc
done chan struct{}
mu sync.Mutex
state market.AuthorizationState
url string
userCode string
err error
version uint64
hasEvent bool
changed chan struct{}
}
func newManagedCredentialAuthorizationProvider(host managedCredentialAuthorizationHost) *managedCredentialAuthorizationProvider {
return &managedCredentialAuthorizationProvider{
host: host, sessions: make(map[string]*credentialBrokerSession), activeByRoute: make(map[string]string),
}
}
func (host *Host) BeginAuthorization(ctx context.Context, request market.AuthorizationStartRequest) (market.AuthorizationSession, error) {
if host == nil || host.authorizationProvider == nil {
return market.AuthorizationSession{}, errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Begin(ctx, request)
}
func (host *Host) DisconnectAuthorization(ctx context.Context, request market.AuthorizationDisconnectRequest) error {
if host == nil || host.authorizationProvider == nil {
return errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Disconnect(ctx, request)
}
func (host *Host) CancelAuthorization(ctx context.Context, request market.AuthorizationCancelRequest) error {
if host == nil || host.authorizationProvider == nil {
return errors.New("connector authorization provider is unavailable")
}
return host.authorizationProvider.Cancel(ctx, request)
}
func (host *Host) InspectAuthorization(ctx context.Context, request market.AuthorizationInspectRequest) (market.AuthorizationObservation, error) {
if host == nil || host.authorizationProvider == nil {
return market.AuthorizationObservation{}, errors.New("connector authorization inspector is unavailable")
}
return host.authorizationProvider.Inspect(ctx, request)
}
func (provider *managedCredentialAuthorizationProvider) Begin(
ctx context.Context,
request market.AuthorizationStartRequest,
) (market.AuthorizationSession, error) {
connector := request.Connector
connector.Release = request.Release
route, err := provider.host.authorizationRoute(ctx, request.Scope, connector)
if err != nil {
return market.AuthorizationSession{}, err
}
session, err := provider.authorizationSessionOrStart(route, request.OperationID, request.StepRevisionBase)
if err != nil {
return market.AuthorizationSession{}, err
}
if err := session.awaitEventAfter(ctx, request.AfterStepRevision); err != nil {
cleanupContext, cancelCleanup := context.WithTimeout(context.Background(), 5*time.Second)
cleanupErr := provider.cancelAuthorizationSession(cleanupContext, session)
cancelCleanup()
return market.AuthorizationSession{}, errors.Join(err, cleanupErr)
}
state, authorizationURL, userCode, stepRevision, sessionErr := session.snapshot()
if sessionErr != nil {
provider.clearAuthorizationSession(request.OperationID, session)
provider.host.releaseAuthorizationRoute(route)
return market.AuthorizationSession{}, sessionErr
}
result := market.AuthorizationSession{
OperationID: request.OperationID,
ConnectorKey: request.Connector.Key,
ConnectionID: route.connectionID,
SessionID: request.OperationID + "/credential-broker",
AuthorizationURL: authorizationURL,
UserCode: userCode,
StepRevision: stepRevision,
State: state,
}
if state == market.AuthorizationStateConnected {
provider.clearAuthorizationSession(request.OperationID, session)
provider.host.releaseAuthorizationRoute(route)
}
return result, nil
}
func (provider *managedCredentialAuthorizationProvider) Cancel(
ctx context.Context,
request market.AuthorizationCancelRequest,
) error {
provider.mu.Lock()
session := provider.sessions[strings.TrimSpace(request.OperationID)]
provider.mu.Unlock()
if session == nil {
return nil
}
return provider.cancelAuthorizationSession(ctx, session)
}
func (provider *managedCredentialAuthorizationProvider) Disconnect(
ctx context.Context,
request market.AuthorizationDisconnectRequest,
) error {
route, err := provider.host.authorizationRoute(ctx, request.Scope, request.Connector)
if err != nil {
return err
}
if session := provider.takeAuthorizationSessionByRoute(route.id); session != nil {
session.cancel()
select {
case <-session.done:
case <-ctx.Done():
return ctx.Err()
}
}
operationContext, cancel := context.WithTimeout(ctx, route.credentialBrokerLaunch.timeout)
defer cancel()
connection, processID, err := provider.host.startCredentialBroker(operationContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "disconnect",
})
if err != nil {
return fmt.Errorf("start connector credential broker disconnect: %w", err)
}
defer func() { _ = route.releaseProcess(processID, connection) }()
event, err := readCredentialBrokerTerminalEvent(operationContext, connection)
if err != nil {
return fmt.Errorf("disconnect connector authorization: %w", err)
}
if event.Type != "disconnected" {
return credentialBrokerEventError(event, "disconnect")
}
provider.host.observeAuthorization(route, market.AuthorizationStateDisconnected)
provider.host.releaseAuthorizationRoute(route)
return nil
}
func (provider *managedCredentialAuthorizationProvider) Inspect(
ctx context.Context,
request market.AuthorizationInspectRequest,
) (market.AuthorizationObservation, error) {
route, err := provider.host.authorizationRoute(ctx, request.Scope, request.Connector)
if err != nil {
return market.AuthorizationObservation{}, err
}
operationContext, cancel := context.WithTimeout(ctx, route.credentialBrokerLaunch.timeout)
defer cancel()
connection, processID, err := provider.host.startCredentialBroker(operationContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "inspect",
})
if err != nil {
return market.AuthorizationObservation{}, fmt.Errorf("start connector credential broker inspect: %w", err)
}
defer func() { _ = route.releaseProcess(processID, connection) }()
event, err := readCredentialBrokerInspectionEvent(operationContext, connection)
if err != nil {
return market.AuthorizationObservation{}, fmt.Errorf("inspect connector authorization: %w", err)
}
var state market.AuthorizationObservationState
switch event.Type {
case "connected":
state = market.AuthorizationObservationConnected
case "disconnected":
state = market.AuthorizationObservationDisconnected
case "expired":
state = market.AuthorizationObservationExpired
case "error":
state = market.AuthorizationObservationFailed
default:
return market.AuthorizationObservation{}, fmt.Errorf("connector credential broker returned unsupported inspect event %q", event.Type)
}
connector := request.Connector
return market.AuthorizationObservation{
AccountID: request.Scope.AccountID, AccountGeneration: request.AccountGeneration,
VMAssignmentID: request.VMAssignmentID, ConnectorKey: connector.Key, ConnectionID: route.connectionID,
ReleaseDigest: connector.Release.ReleaseDigest, AuthorizationSessionID: request.AuthorizationSessionID,
AuthorizationGeneration: request.AuthorizationGeneration, DesktopBootEpoch: request.DesktopBootEpoch,
GuestBootID: request.GuestBootID, RuntimeEpoch: request.RuntimeEpoch, StateRevision: request.StateRevision,
State: state, Reason: strings.TrimSpace(event.Message), FailureCode: strings.TrimSpace(event.Code), ObservedAt: time.Now().UTC(),
}, nil
}
func (provider *managedCredentialAuthorizationProvider) authorizationSessionOrStart(
route *connectorRoute,
operationID string,
stepRevisionBase uint64,
) (*credentialBrokerSession, error) {
operationID = strings.TrimSpace(operationID)
provider.mu.Lock()
if session := provider.sessions[operationID]; session != nil {
_, _, _, _, sessionErr := session.snapshot()
if sessionErr == nil {
provider.mu.Unlock()
return session, nil
}
// A broker can fail after emitting its first authorization URL. Do not
// make the next user action consume that terminal error merely to clear
// the cache; replace it with a fresh broker in this same request.
delete(provider.sessions, operationID)
if provider.activeByRoute[route.id] == operationID {
delete(provider.activeByRoute, route.id)
}
}
if activeOperationID := provider.activeByRoute[route.id]; activeOperationID != "" {
active := provider.sessions[activeOperationID]
if active != nil {
_, _, _, _, activeErr := active.snapshot()
if activeErr != nil {
select {
case <-active.done:
delete(provider.sessions, activeOperationID)
delete(provider.activeByRoute, route.id)
default:
provider.mu.Unlock()
return nil, errors.New("connector credential broker termination is still pending")
}
} else {
provider.mu.Unlock()
return nil, errors.New("connector credential broker has an active authorization attempt")
}
} else {
delete(provider.activeByRoute, route.id)
}
}
processContext, cancel := context.WithTimeout(context.Background(), route.credentialBrokerLaunch.timeout)
connection, processID, err := provider.host.startCredentialBroker(processContext, route, credentialBrokerRequest{
Protocol: market.CredentialBrokerProtocolV1, Operation: "begin",
})
if err != nil {
cancel()
provider.mu.Unlock()
return nil, fmt.Errorf("start connector credential broker: %w", err)
}
session := &credentialBrokerSession{
operationID: operationID, route: route, connection: connection, cancel: cancel,
done: make(chan struct{}), version: stepRevisionBase, changed: make(chan struct{}),
}
provider.sessions[operationID] = session
provider.activeByRoute[route.id] = operationID
provider.mu.Unlock()
go consumeAuthorizationEvents(processContext, provider.host, route, connection, processID, session)
return session, nil
}
func consumeAuthorizationEvents(
ctx context.Context,
host managedCredentialAuthorizationHost,
route *connectorRoute,
connection agentruntime.ProcessConnection,
processID uint64,
session *credentialBrokerSession,
) {
defer session.cancel()
defer close(session.done)
defer func() { _ = route.releaseProcess(processID, connection) }()
var stdout, stderr strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
// Explicit cancellation owns the terminal state transition. Do not race
// the caller by projecting a transient broker failure while shutdown is
// already in progress.
if errors.Is(ctx.Err(), context.Canceled) {
return
}
if !errors.Is(err, io.EOF) {
failAuthorizationSession(host, route, session, fmt.Errorf("receive connector credential broker event: %w", err))
} else if !session.terminal() {
failAuthorizationSession(host, route, session, errors.New("connector credential broker exited before a terminal event"))
}
return
}
stdout.Write(frame.Stdout)
stderr.Write(frame.Stderr)
if stdout.Len()+stderr.Len() > 1<<20 {
failAuthorizationSession(host, route, session, errors.New("connector credential broker output exceeded its limit"))
return
}
for {
line, remaining, ok := strings.Cut(stdout.String(), "\n")
if !ok {
break
}
stdout.Reset()
stdout.WriteString(remaining)
if err := applyCredentialBrokerEvent(host, route, session, line); err != nil {
failAuthorizationSession(host, route, session, err)
return
}
}
if frame.ExitCode != nil {
if trailing := strings.TrimSpace(stdout.String()); trailing != "" {
if err := applyCredentialBrokerEvent(host, route, session, trailing); err != nil {
failAuthorizationSession(host, route, session, err)
return
}
}
if *frame.ExitCode != 0 && !session.terminal() {
failAuthorizationSession(host, route, session, fmt.Errorf("connector credential broker exited with code %d: %s", *frame.ExitCode, boundedBrokerMessage(stderr.String())))
} else if !session.terminal() {
failAuthorizationSession(host, route, session, errors.New("connector credential broker exited before a terminal event"))
}
return
}
}
}
func applyCredentialBrokerEvent(host managedCredentialAuthorizationHost, route *connectorRoute, session *credentialBrokerSession, payload string) error {
if strings.TrimSpace(payload) == "" {
return nil
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(payload, &event); err != nil {
return fmt.Errorf("decode connector credential broker event: %w", err)
}
switch event.Type {
case "authorization_url":
if !safeCredentialBrokerURL(event.URL, route.credentialBrokerLaunch.allowedHosts) {
return errors.New("connector credential broker returned an unauthorized URL")
}
userCode, err := normalizeCredentialBrokerUserCode(event.Code)
if err != nil {
return err
}
session.update(market.AuthorizationStatePending, event.URL, userCode, nil)
host.observeAuthorization(route, market.AuthorizationStatePending)
case "connected":
session.update(market.AuthorizationStateConnected, "", "", nil)
host.observeAuthorization(route, market.AuthorizationStateConnected)
case "error":
return credentialBrokerEventError(event, "authorize")
default:
return fmt.Errorf("connector credential broker returned unsupported event type %q", event.Type)
}
return nil
}
func (host *Host) authorizationRoute(ctx context.Context, scope market.OperationScope, connector market.Connector) (*connectorRoute, error) {
if host == nil {
return nil, errors.New("managed connector authorization is unavailable")
}
releaseLane := host.enterConnectorLane(connector.Key)
defer releaseLane()
managed := connector.Release.Manifest.Implementation.ManagedStdio
if connector.Release.Manifest.Implementation.Kind != market.ImplementationKindManagedStdio ||
connector.Release.Manifest.AuthorizationKind == "none" || managed == nil || managed.CredentialBroker == nil ||
!installationTargetsRelease(connector.Installation, connector.Release.ReleaseDigest) {
return nil, errors.New("managed connector authorization is unavailable")
}
connectionID := "default"
if accountID := strings.TrimSpace(scope.AccountID); accountID != "" {
connectionID = market.AccountRuntimeConnectionID(accountID, connector.Key)
}
key := connectorRouteKey(connectionID, connector.Key)
if route, _ := host.routes.Route(key).(*connectorRoute); route != nil && host.routeCurrent(route) && route.credentialBrokerLaunch != nil {
return route, nil
}
host.authorizationMu.Lock()
if route := host.authorizationRoutes[key]; route != nil && route.releaseDigest == connector.Release.ReleaseDigest &&
route.credentialBrokerLaunch != nil && !route.processes.IsFenced() {
host.authorizationMu.Unlock()
return route, nil
}
host.authorizationMu.Unlock()
route, err := host.buildAuthorizationRoute(ctx, connectionID, connector)
if err != nil {
return nil, err
}
host.authorizationMu.Lock()
current := host.authorizationRoutes[key]
if current == nil || current.releaseDigest != connector.Release.ReleaseDigest || current.processes.IsFenced() {
host.authorizationRoutes[key] = route
host.authorizationMu.Unlock()
if current != nil {
current.Fence()
_ = current.Close(time.Now().Add(3 * time.Second))
}
return route, nil
}
host.authorizationMu.Unlock()
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
return current, nil
}
func (host *Host) buildAuthorizationRoute(ctx context.Context, connectionID string, connector market.Connector) (*connectorRoute, error) {
if err := market.ValidateRuntimeReleaseShape(connector.Release); err != nil {
return nil, err
}
if !installationTargetsRelease(connector.Installation, connector.Release.ReleaseDigest) {
return nil, errors.New("managed connector authorization release is not installed")
}
prepared, err := host.artifacts.ResolvePrepared(ctx, connector.Release)
if err != nil {
return nil, fmt.Errorf("resolve prepared connector artifact for authorization: %w", err)
}
installedRoot := prepared.PreparedPath
executionRoot, err := host.snapshots.Create(prepared, artifactNativeEntrypoints(connector.Release)...)
if err != nil {
return nil, fmt.Errorf("create connector authorization snapshot: %w", err)
}
prepared.PreparedPath = executionRoot
generation := connector.Revision
if generation == 0 {
generation = 1
}
runtimeRequest := market.RuntimeReconcileRequest{
ConnectionID: connectionID,
Connector: connector,
Generation: market.HostGeneration{BootEpoch: "authorization", Generation: generation},
}
plan, err := host.planner.Build(ctx, runtimeRequest, prepared)
if err != nil {
_ = host.snapshots.Remove(executionRoot)
return nil, err
}
route := newConnectorRoute(runtimeRequest)
route.userHome = plan.UserHome
route.executionRoot = executionRoot
route.installedRoot = installedRoot
route.snapshots = host.snapshots
if plan.Managed.CLI == nil || plan.Managed.CredentialBroker == nil {
_ = route.Close(time.Now().Add(3 * time.Second))
return nil, errors.New("managed connector authorization requires a CLI credential broker")
}
if err := host.attachCLI(route, plan.Managed, prepared, plan.InstalledCLI, plan.Executable, plan.StateDir, plan.ArtifactTrees); err != nil {
_ = route.Close(time.Now().Add(3 * time.Second))
return nil, err
}
if err := host.attachCredentialBroker(route, plan.Managed.CredentialBroker, prepared, plan.Executable, plan.StateDir, plan.ArtifactTrees); err != nil {
_ = route.Close(time.Now().Add(3 * time.Second))
return nil, err
}
return route, nil
}
func (host *Host) observeAuthorization(route *connectorRoute, state market.AuthorizationState) {
if host == nil || host.authorizationObserver == nil || route == nil {
return
}
host.authorizationObserver.ObserveAuthorization(context.Background(), AuthorizationObservation{
ConnectorKey: route.connectorKey, ConnectionID: route.connectionID, State: state, ObservedAt: time.Now().UTC(),
})
}
func (host *Host) releaseAuthorizationRoute(route *connectorRoute) {
if host == nil || route == nil {
return
}
host.authorizationMu.Lock()
if host.authorizationRoutes[route.id] == route {
delete(host.authorizationRoutes, route.id)
} else {
host.authorizationMu.Unlock()
return
}
host.authorizationMu.Unlock()
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
}
func (host *Host) releaseAuthorizationRouteByKey(key string) {
if host == nil {
return
}
host.authorizationMu.Lock()
route := host.authorizationRoutes[key]
delete(host.authorizationRoutes, key)
host.authorizationMu.Unlock()
if route != nil {
route.Fence()
_ = route.Close(time.Now().Add(3 * time.Second))
}
}
func failAuthorizationSession(host managedCredentialAuthorizationHost, route *connectorRoute, session *credentialBrokerSession, err error) {
session.fail(err)
host.observeAuthorization(route, market.AuthorizationStateFailed)
}
func (host *Host) startCredentialBroker(
ctx context.Context,
route *connectorRoute,
request credentialBrokerRequest,
) (agentruntime.ProcessConnection, uint64, error) {
launch := route.credentialBrokerLaunch
if launch == nil || request.Protocol != market.CredentialBrokerProtocolV1 ||
(request.Operation != "begin" && request.Operation != "inspect" && request.Operation != "disconnect") {
return nil, 0, errors.New("connector credential broker request is invalid")
}
spec := connectorruntime.ConnectorProcessSpec(route.connectionID, route.connectorKey, launch.language, launch.executable,
launch.cwd, []string{launch.entrypoint}, launch.stateDir, route.userHome, launch.artifactTrees)
cliLaunch, err := json.Marshal(launch.cliLaunch)
if err != nil {
return nil, 0, err
}
spec.Env = append(spec.Env,
"TUTTI_CONNECTOR_CREDENTIAL_BROKER_PROTOCOL="+market.CredentialBrokerProtocolV1,
"TUTTI_CONNECTOR_CLI_LAUNCH_JSON="+string(cliLaunch),
)
connection, processID, err := host.startProcess(ctx, route, spec, false)
if err != nil {
return nil, 0, err
}
payload, err := json.Marshal(request)
if err == nil {
err = connection.Send(append(payload, '\n'))
}
if err == nil {
if graceful, ok := connection.(agentruntime.GracefulProcessConnection); ok {
err = graceful.CloseInput()
}
}
if err != nil {
_ = route.releaseProcess(processID, connection)
return nil, 0, err
}
return connection, processID, nil
}
func receiveCredentialBrokerFrameContext(ctx context.Context, connection agentruntime.ProcessConnection) (agentruntime.ProcessFrame, error) {
if contextual, ok := connection.(agentruntime.ContextProcessConnection); ok {
return contextual.RecvContext(ctx)
}
return connection.Recv()
}
func readCredentialBrokerTerminalEvent(ctx context.Context, connection agentruntime.ProcessConnection) (credentialBrokerEvent, error) {
var output strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
return credentialBrokerEvent{}, err
}
output.Write(frame.Stdout)
if output.Len() > 1<<20 {
return credentialBrokerEvent{}, errors.New("connector credential broker output exceeded its limit")
}
for {
line, remaining, ok := strings.Cut(output.String(), "\n")
if !ok {
break
}
output.Reset()
output.WriteString(remaining)
if strings.TrimSpace(line) == "" {
continue
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(line, &event); err != nil {
return credentialBrokerEvent{}, err
}
if event.Type == "disconnected" || event.Type == "error" {
return event, nil
}
return credentialBrokerEvent{}, fmt.Errorf("unexpected credential broker disconnect event %q", event.Type)
}
if frame.ExitCode != nil {
return credentialBrokerEvent{}, errors.New("connector credential broker exited before disconnect completed")
}
}
}
func readCredentialBrokerInspectionEvent(ctx context.Context, connection agentruntime.ProcessConnection) (credentialBrokerEvent, error) {
var output strings.Builder
for {
frame, err := receiveCredentialBrokerFrameContext(ctx, connection)
if err != nil {
return credentialBrokerEvent{}, err
}
output.Write(frame.Stdout)
if output.Len() > 1<<20 {
return credentialBrokerEvent{}, errors.New("connector credential broker output exceeded its limit")
}
for {
line, remaining, ok := strings.Cut(output.String(), "\n")
if !ok {
break
}
output.Reset()
output.WriteString(remaining)
if strings.TrimSpace(line) == "" {
continue
}
var event credentialBrokerEvent
if err := decodeCredentialBrokerEvent(line, &event); err != nil {
return credentialBrokerEvent{}, err
}
switch event.Type {
case "connected", "disconnected", "expired", "error":
return event, nil
default:
return credentialBrokerEvent{}, fmt.Errorf("unexpected credential broker inspect event %q", event.Type)
}
}
if frame.ExitCode != nil {
return credentialBrokerEvent{}, errors.New("connector credential broker exited before inspect completed")
}
}
}
func decodeCredentialBrokerEvent(payload string, event *credentialBrokerEvent) error {
decoder := json.NewDecoder(strings.NewReader(payload))
decoder.DisallowUnknownFields()
if err := decoder.Decode(event); err != nil {
return err
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("credential broker event contains trailing JSON")
}
return nil
}
func safeCredentialBrokerURL(value string, allowedHosts map[string]struct{}) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
if err != nil || parsed.Scheme != "https" || parsed.User != nil || parsed.Hostname() == "" || parsed.Port() != "" {
return false
}
_, allowed := allowedHosts[strings.ToLower(parsed.Hostname())]
return allowed
}
func credentialBrokerEventError(event credentialBrokerEvent, operation string) error {
message := boundedBrokerMessage(event.Message)
if message == "" {
message = "connector credential broker reported an error"
}
if code := strings.TrimSpace(event.Code); code != "" {
return fmt.Errorf("%s connector authorization (%s): %s", operation, code, message)
}
return fmt.Errorf("%s connector authorization: %s", operation, message)
}
func boundedBrokerMessage(message string) string {
message = strings.TrimSpace(message)
if len(message) > 4096 {
message = message[len(message)-4096:]
}
return message
}
func normalizeCredentialBrokerUserCode(value string) (string, error) {
value = strings.TrimSpace(value)
if utf8.RuneCountInString(value) > 128 {
return "", errors.New("connector credential broker returned an oversized device code")
}
return value, nil
}
func (session *credentialBrokerSession) awaitEventAfter(ctx context.Context, afterStepRevision uint64) error {
session.mu.Lock()
hasEvent := session.hasEvent
session.mu.Unlock()
wait := credentialBrokerContinuationWait
if !hasEvent {
wait = credentialBrokerInitialEventTimeout
}
timer := time.NewTimer(wait)
defer timer.Stop()
for {
session.mu.Lock()
hasEvent = session.hasEvent
version := session.version
state := session.state
changed := session.changed
session.mu.Unlock()
if hasEvent && (version > afterStepRevision || state == market.AuthorizationStateConnected || state == market.AuthorizationStateFailed) {
return nil
}
select {
case <-changed:
continue
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
session.mu.Lock()
hasEvent = session.hasEvent
session.mu.Unlock()
if !hasEvent {
return errors.New("connector credential broker did not return an initial event")
}
return nil
}
}
}
func (session *credentialBrokerSession) update(state market.AuthorizationState, authorizationURL, userCode string, err error) {
session.mu.Lock()
session.state = state
session.url = authorizationURL
session.userCode = userCode
session.err = err
session.version++
session.hasEvent = true
close(session.changed)
session.changed = make(chan struct{})
session.mu.Unlock()
}
func (session *credentialBrokerSession) fail(err error) {
session.update(market.AuthorizationStateFailed, "", "", err)
}
func (session *credentialBrokerSession) snapshot() (market.AuthorizationState, string, string, uint64, error) {
session.mu.Lock()
defer session.mu.Unlock()
return session.state, session.url, session.userCode, session.version, session.err
}
func (session *credentialBrokerSession) terminal() bool {
session.mu.Lock()
defer session.mu.Unlock()
return session.state == market.AuthorizationStateConnected || session.err != nil
}
func (provider *managedCredentialAuthorizationProvider) takeAuthorizationSessionByRoute(routeID string) *credentialBrokerSession {
provider.mu.Lock()
defer provider.mu.Unlock()
operationID := provider.activeByRoute[routeID]
session := provider.sessions[operationID]
delete(provider.activeByRoute, routeID)
delete(provider.sessions, operationID)
return session
}
func (provider *managedCredentialAuthorizationProvider) cancelAuthorizationSession(
ctx context.Context,
session *credentialBrokerSession,
) error {
if provider == nil || session == nil {
return nil
}
session.cancel()
var closeErr error
if session.connection != nil {
closeErr = session.connection.Close()
}
select {
case <-session.done:
provider.clearAuthorizationSession(session.operationID, session)
provider.host.releaseAuthorizationRoute(session.route)
return closeErr
case <-ctx.Done():
return errors.Join(closeErr, fmt.Errorf("wait for connector credential broker termination: %w", ctx.Err()))
}
}
func (provider *managedCredentialAuthorizationProvider) cancelAuthorizationSessionByRoute(routeID string) {
if provider == nil {
return
}
if session := provider.takeAuthorizationSessionByRoute(routeID); session != nil {
session.cancel()
if session.connection != nil {
_ = session.connection.Close()
}
}
}
func (provider *managedCredentialAuthorizationProvider) clearAuthorizationSession(operationID string, session *credentialBrokerSession) {
provider.mu.Lock()
if provider.sessions[operationID] == session {
delete(provider.sessions, operationID)
}
if session != nil && provider.activeByRoute[session.route.id] == operationID {
delete(provider.activeByRoute, session.route.id)
}
provider.mu.Unlock()
}