api

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 41 Imported by: 0

Documentation

Overview

Package api is the conductor's own HTTP surface: the AgentService handlers, the bearer that guards them, and the health and metrics endpoints. There is no other way in — the listener is loopback by default and podium-server is what a browser reaches it through.

Index

Constants

View Source
const (
	ScopeHeader = "X-Podium-Scope"
	ScopeAll    = "all"
	// InfraHeader is set by podium-server when the caller may edit the Slack bot's MCP
	// list: the dev token, or a signed-in admin or owner. A member does not get it.
	InfraHeader = "X-Podium-Infra"
)

ScopeHeader is what podium-server's proxy sets so the conductor can tell a shared dev token from a signed-in person. The proxy strips any client-supplied copy. ScopeAll is the only value that widens a list; anything else, including a missing header, is one login's own chats.

View Source
const (
	DevInboundPath  = "/dev/inbound"
	DevOutboundPath = "/dev/outbound"
)

Dev source routes. They exist only when PODIUM_AGENT_DEV_SOURCE is true and they sit behind the same bearer as the AgentService.

View Source
const (
	OAuthPending  = "pending"
	OAuthSlowDown = "slow_down"
	OAuthDone     = "done"
	OAuthDenied   = "denied"
	OAuthExpired  = "expired"
)

The states PollProviderOAuth reports. They are the whole client contract: everything the provider can answer maps onto one of these five.

View Source
const (
	ProviderAnthropic = profiles.ProviderAnthropic
	ProviderXAI       = profiles.ProviderXAI
	ProviderOpenAI    = profiles.ProviderOpenAI
	ProviderGoogle    = profiles.ProviderGoogle
)

The providers this control plane knows. A provider is a set of credentials and an endpoint; profiles.Backends is what says which agent backend spends which of them.

View Source
const (
	AuthAPIKey = "api_key"
	AuthOAuth  = "oauth"
)

How a stored credential was obtained. The distinction is not cosmetic: an API key does not expire and an access token does, so only one of the two is refreshed and only one of the two has a hint worth showing.

View Source
const (
	CredentialPersonal = "personal"
	CredentialGlobal   = "global"
)

Whose credential a turn spends, as ListAgents reports it.

View Source
const ClaudeAgentCredential = "claude-agent"

ClaudeAgentCredential is what HostCredential is asked for to get the global Claude subscription: the name of opencode's provider that spends it.

View Source
const DefaultUsageDays = 30

DefaultUsageDays is the range GetUsage reads when the request names neither end.

View Source
const LoginHeader = "X-Podium-Login"

LoginHeader is what podium-server's proxy sets to the calling operator's login. Any client-supplied copy is stripped there, so what arrives here is the server's word.

View Source
const PullRequestMCPRESTPath = "/v1/mcp/pull-request"

PullRequestMCPRESTPath is the pull-request reader among the resources.

View Source
const TurnRESTPrefix = "/v1/"

TurnRESTPrefix is where these resources live, on the conductor and under podium-server's /turn-surface.

View Source
const TurnTokenHeader = "X-Podium-Turn"

TurnTokenHeader carries a turn's token. A header and not a request field: it is a credential, and a credential in a message body is a credential in every log that ever prints a request.

Variables

This section is empty.

Functions

func Infra added in v0.4.0

func Infra(ctx context.Context) bool

Infra reports whether this caller may edit the Slack bot's MCP list. The proxy sets the header for the dev token and for an admin or owner. A member's request does not.

func Login

func Login(ctx context.Context) string

Login is the operator the server says is calling, or "unknown" when a handler was reached without the middleware.

func NewTurnREST added in v0.8.0

func NewTurnREST(turns *TurnService, gitcred *GitCredentialService, prMCP http.Handler) http.Handler

NewTurnREST returns the resources above. A nil service leaves its routes out; prMCP is the pull-request reader, mounted as it is.

func ReloadProfile

func ReloadProfile(ctx context.Context, st *store.Store, live *profiles.Live) (*profiles.Profile, error)

ReloadProfile rebuilds the profile a turn runs from and swaps it into live.

A saved assistant definition replaces the assistant fields of the install file. Until one has been saved, the legacy override row is applied instead. Playbooks come from the profile directory either way.

This is the whole of "a change reaches a running conductor without a restart": every reader takes live.Current() per use, so the swap is the only thing that has to happen. It is a plain function because the process calls it at boot, before the AgentService exists, as well as on every override write and on a timer.

func ReloadProfileDir

func ReloadProfileDir(
	ctx context.Context, st *store.Store, live *profiles.Live, dir string,
) (*profiles.Profile, error)

ReloadProfileDir is the same rebuild after re-reading the profile directory off disk, which is the half a restart used to be the only way to change: profile.yaml, playbooks/*.yaml and every prompt a `file:` points at.

**Nothing is swapped until all of it succeeds.** A directory in the middle of being edited is a load error, and a load error has to leave the conductor running the profile it already has rather than half of a new one. That is also why this is only ever called for an operator who asked: a timer re-reading a file somebody is saving would be a way to break a working bot by touching a keyboard.

func RequireBearer

func RequireBearer(token string, next http.Handler) http.Handler

RequireBearer refuses everything that does not present the conductor's token, and puts the proxied login in the request context. The comparison is constant time: the token is the only thing standing between a local process and every session the bot has had.

func SeesAll added in v0.4.0

func SeesAll(ctx context.Context) bool

SeesAll reports whether this request is the shared dev token. A signed-in user is not, whatever login header they might have hoped to send: the proxy is what sets the scope.

Types

type AgentService

type AgentService struct {
	// contains filtered or unexported fields
}

AgentService implements podium.agent.v1.AgentService.

func NewAgentService

func NewAgentService(opts AgentServiceOptions) *AgentService

NewAgentService returns the handlers.

func (*AgentService) AttachChatPullRequest

AttachChatPullRequest links a pull request to one of the caller's chats by hand.

func (*AgentService) ClearGitHubConnection added in v0.5.0

ClearGitHubConnection drops the saved App and turns it off in the running conductor.

func (*AgentService) ClearMcpServerToken

ClearMcpServerToken removes the credential and leaves the registration. The goal state is "no token", so a NotFound from the control plane is success: calling it twice is not an error.

func (*AgentService) ClearProviderKey

ClearProviderKey removes the credential and the metadata. The goal state is "no credential", so a NotFound from the control plane is success: calling it twice is not an error.

func (*AgentService) ClearSlackConnection added in v0.5.0

ClearSlackConnection drops the saved pair and falls back to the environment's, or to no Slack at all.

func (*AgentService) CompleteGitHubOAuth added in v0.6.0

CompleteGitHubOAuth trades the code for a user token and stores it under the person who started the flow, which must also be the person completing it.

func (*AgentService) CompleteMcpOAuth

CompleteMcpOAuth trades the code for a token and stores it as the server's credential.

From here on there is no difference between a sign-in and a pasted token: the access token goes into the same Podium secret, so the brief, the task spec and the runtime cannot tell them apart. What the sign-in leaves extra is the bag that lets it be refreshed.

func (*AgentService) CreateChat

CreateChat opens a conversation owned by the caller.

func (*AgentService) CreateMcpServer

CreateMcpServer registers a new server, and stores its token when one came with it.

The order is the registration first and the token second, which is the opposite of SetProviderKey's. A provider key is validated with the provider before anything is written; an MCP server cannot be asked whether it is there without also being handed the credential, so there is nothing to validate against and no reason to write the secret before the row that owns it.

func (*AgentService) CreatePersonality added in v0.6.0

CreatePersonality stores a voice for the caller. The request name is ignored. The store generates one from the display name.

func (*AgentService) CreatePlaybook

CreatePlaybook writes playbooks/<name>.yaml and re-reads the profile directory.

func (*AgentService) DeleteChat

DeleteChat removes one of the caller's chats and every message in it.

A task still answering the chat is cancelled first. The transcript is about to disappear, and a container nobody is listening to would otherwise keep the node slot and can still finish work the person who asked for it will never see. Cancel does not wait: the chat is gone immediately, the node has up to 30s. Sessions and turns stay.

func (*AgentService) DeleteMcpServer

DeleteMcpServer removes a registration and the token with it.

A server a playbook still names is deletable, exactly as a skill is: profiles deliberately does not check that a named server exists — a playbook file has to load on a machine with no database — so refusing here would be the only place in Podium where the two disagreed. The playbook's turns then fail naming the server, and ListMcpServers reports which playbooks name each one so a human can see that before pressing the button.

The secret goes first. A row that is gone with a credential left behind it is a credential nothing in the UI can see, name or remove.

func (*AgentService) DeleteMemory

DeleteMemory takes one memory out of every future recall.

The memory engine has no single-memory delete, so this is its curation tombstone: the memory is excluded from recall, from consolidation and from the graph, the observations derived from it are pruned, and the row is kept in an archive. What the operator asked for holds — no future turn sees it, and neither does this UI — but the record of it having existed is not destroyed.

func (*AgentService) DeletePersonality added in v0.6.0

DeletePersonality removes one of the caller's voices. Chats that used it stay.

func (*AgentService) DeletePlaybook

DeletePlaybook removes playbooks/<name>.yaml.

func (*AgentService) DeleteSkill

DeleteSkill removes the skill directory.

func (*AgentService) DetachChatPullRequest

DetachChatPullRequest takes one link off one of the caller's chats.

func (*AgentService) DisconnectGitHubAccount added in v0.6.0

DisconnectGitHubAccount deletes the caller's token and record. Doing it twice is not an error.

func (*AgentService) GetConnections added in v0.5.0

GetConnections reports Slack, GitHub and Linear without any secret value.

func (*AgentService) GetGitHubAccount added in v0.6.0

GetGitHubAccount answers for the caller only.

func (*AgentService) GetProfile

GetProfile reports the profile in force and every playbook in full.

A playbook's secrets are reported by NAME, which is all a playbook file holds either. No value reaches this response: Podium has no endpoint that reads a secret's value back, and a playbook naming one is not a privilege — a task spec names secrets exactly the same way.

func (*AgentService) GetProfileFile added in v0.3.2

GetProfileFile returns profile.yaml as it is on disk.

func (*AgentService) GetRunsOn added in v0.7.0

GetRunsOn reports where a turn runs when its playbook does not say. Never saved is the fleet's own machines.

func (*AgentService) GetSession

GetSession reads one session by id.

func (*AgentService) GetSettings

GetSettings reports what the conductor is configured with.

key_set means "an agent turn will find a credential", so it comes from the **control plane**, not from the metadata row: the secret is the control plane's and `podium secret rm` can take it away without the conductor hearing. The row only supplies the hint, the login and the time, and only when it still describes the version that is actually stored. Nothing here reads a value — there is no endpoint that could.

func (*AgentService) GetUsage

GetUsage reports what the conductor spent over a range: one row per day for a calendar, and the per-task cost of each turn inside it.

func (*AgentService) HostCredential

func (s *AgentService) HostCredential(ctx context.Context, provider string) (string, error)

HostCredential is the bearer a host turn spends for one provider, read back from the provider's global Podium secret: the same one a container turn is given. A person's own is PersonalHostCredential.

ClaudeAgentCredential asks for the global Claude subscription instead, which only the chat host's claude-agent provider spends.

func (*AgentService) ListAgents

ListAgents is the picker's catalogue: the backends this conductor can run a turn on, the models each offers, and whether a credential for it is actually stored.

func (*AgentService) ListChats

ListChats returns chats newest first. A signed-in user gets their own web chats. The dev token, which the proxy marks with X-Podium-Scope: all, gets every chat.

func (*AgentService) ListMcpServers

ListMcpServers reports every registered server.

func (*AgentService) ListMemories

ListMemories pages through the shared memory, newest first.

func (*AgentService) ListPersonalities added in v0.6.0

ListPersonalities returns the caller's voices. Podium is not one of them.

func (*AgentService) ListPlaybooks

ListPlaybooks reports who answers a conversation and what that turn may delegate to.

It is the chat's surface and stays minimal: per playbook a name, an image and the first line of a prompt the operator wrote. The whole definition — tools, secrets, resources — is what GetProfile is for.

func (*AgentService) ListSessions

ListSessions returns conversations newest first.

func (*AgentService) ListSkills

ListSkills reports every skill this conductor can hand a turn: the directories under PODIUM_AGENT_SKILLS_DIR. A directory that is there and will not load is reported with the reason attached rather than hidden.

func (*AgentService) ListSlackChannels added in v0.2.0

ListSlackChannels reports every known Slack channel. When Slack is connected it also refreshes names from membership, which is how a channel the bot was invited to appears before anyone has mentioned it.

func (*AgentService) ListTurns

ListTurns returns one session's turns, newest first.

func (*AgentService) MoveCredentialsIntoStore added in v0.6.0

func (s *AgentService) MoveCredentialsIntoStore(ctx context.Context)

MoveCredentialsIntoStore moves the credentials a database written before the secret store answered the conductor still holds in clear: providers' refresh tokens and bearer copies, and MCP sign-ins' refresh tokens and client secrets. A failure is logged and the move is tried again on the next start, or for a provider on its next read.

func (*AgentService) PersonalHostCredential added in v0.8.0

func (s *AgentService) PersonalHostCredential(ctx context.Context, login, provider string) (string, error)

PersonalHostCredential is login's own key for provider — the personal secret spec.PersonalModelKeySecretName — or "" when they set none, which is not an error: the company's key is spent instead.

func (*AgentService) PollProviderOAuth

PollProviderOAuth asks the provider whether the human has approved yet, and stores the credential on the one call that comes back authorised.

The token is validated against the provider's own API before it is stored, exactly as a pasted key is. That is not ceremony: xAI's OAuth surface has its own allow-list, so a sign-in can succeed and still produce a token that cannot call the API — and finding that out now, with the provider's own sentence, beats finding it out on the first turn.

func (*AgentService) Reconcile

func (s *AgentService) Reconcile(ctx context.Context) error

Reconcile is the timer's rebuild: the saved assistant, re-read on an interval so a row changed by another conductor or by psql lands here too. A conductor that has not saved one still re-reads the legacy override row.

It takes the write lock, and that is the whole reason it exists as a method of its own. A rebuild is a read of the file half, a read of the database and then a swap; a timer doing that unlocked could read the files, have ReloadProfileDir replace them underneath it, and then swap in a profile built from a directory nobody is running any more — undoing an operator's re-read seconds after they asked for it.

func (*AgentService) RefreshTokens

func (s *AgentService) RefreshTokens(ctx context.Context)

RefreshTokens keeps stored OAuth credentials alive until the process stops — the subscription sign-in's, and every signed-in MCP server's.

It runs as its own goroutine because nothing else is watching: a token issued for an hour would otherwise stop working an hour after a sign-in, and the first anybody would hear of it is a turn failing. A refresh that fails is logged and retried on the next tick — the stored token is left alone, because a token that still has thirty minutes on it is more use than no token at all.

func (*AgentService) ReloadProfileDir

ReloadProfileDir re-reads the profile directory and swaps what it finds in.

The failure is an InvalidArgument and not an Internal on purpose: what fails here is almost always the YAML the operator has just written, and the message profiles.Load returns names the file and the line. Nothing is swapped when it does, so the stale reason is not touched either — the conductor is still running the profile it was, and the error in front of the operator is the whole report.

func (*AgentService) RenameChat

RenameChat changes the title of one of the caller's chats.

func (*AgentService) SearchMemories

SearchMemories is the semantic search behind the Memory tab's search box.

func (*AgentService) SendChatMessage

SendChatMessage stores one human message and starts a turn on it.

func (*AgentService) SetGitHubConnection added in v0.5.0

SetGitHubConnection validates the App the way start-up does, tries it against GitHub, stores it and switches the running conductor to it.

func (*AgentService) SetGitHubWebhookFilters added in v0.7.1

SetGitHubWebhookFilters stores the webhook filters. The source reads them on every delivery, so they apply without a restart.

func (*AgentService) SetMcpServerToken

SetMcpServerToken stores the credential a server authenticates with.

It is not validated, because there is no way to validate it. An MCP server has no unauthenticated "are you there" call, so the only test of a token is a turn using it — which is why the failure an operator sees for a bad token is a turn's, and why the hint exists at all.

func (*AgentService) SetProviderKey

SetProviderKey validates a key with the provider and, only then, stores it.

The order is the contract: validate, write the secret, write the metadata. An operator who reads "saved" has to be able to trust that agents will run, so an unvalidated key is never written, and a metadata row never claims a key the secret store does not hold.

func (*AgentService) SetRunning added in v0.7.1

func (s *AgentService) SetRunning(cfg config.Config, slack SlackDirectory)

SetRunning records the connections the conductor now holds open: the configuration it is running and its Slack source, nil when Slack is off.

func (*AgentService) SetRunsOn added in v0.7.0

SetRunsOn saves where a turn runs when its playbook does not say. A turn already running stays where it is.

func (*AgentService) SetSkillEnabled

SetSkillEnabled has no file to flip: a directory skill is on if it is on disk.

func (*AgentService) SetSlackChannelDescription added in v0.2.0

SetSlackChannelDescription stores the operator note a turn of this channel is briefed with.

func (*AgentService) SetSlackConnection added in v0.5.0

SetSlackConnection validates the pair, tries it against Slack, stores it and switches the running conductor to it. restart_required is left true only when that switch failed.

func (*AgentService) StartGitHubOAuth added in v0.6.0

StartGitHubOAuth answers with the URL to send the caller's browser to.

func (*AgentService) StartMcpOAuth

StartMcpOAuth discovers a server's OAuth, registers this conductor as a client of it, and answers with the URL to send the operator's browser to.

Everything expensive happens here rather than on the callback, so that a server which advertises no OAuth, or refuses a registration, is a refusal the operator gets while looking at the button they pressed — not a dead end after a round trip through a consent screen.

func (*AgentService) StartProviderOAuth

StartProviderOAuth asks the provider for a code a human can approve, and remembers the device code that goes with it. It stores no credential: only a poll that comes back authorised does that.

func (*AgentService) StreamChat

StreamChat replays a chat and then follows it live.

The order is: subscribe, send the turn state, replay, then forward. Subscribing first is what closes the gap — a message stored between the replay and the subscription would otherwise be seen by nobody — and the cost is that a frame may arrive twice, which is harmless because a client keys on seq.

The status frame goes FIRST and unconditionally, for two reasons. It is what tells a browser joining mid-turn that the composer is disabled without waiting for the next progress line. And it is what puts the response headers on the wire immediately: a Connect server stream writes no headers until its first message, and podium-server's proxy gives the conductor 30 seconds to produce them — so a subscriber on a quiet chat would otherwise be disconnected every 30 seconds and reconnect for ever.

It never returns on its own. The client cancels, and the request context is what unwinds the subscription.

func (*AgentService) UpdateMcpServer

UpdateMcpServer replaces a registration. The token is untouched: it belongs to the name, and the name is what cannot be edited here.

func (*AgentService) UpdatePersonality added in v0.6.0

UpdatePersonality replaces the display name, instructions, and model of one of the caller's voices. The request name is ignored, and the stored name stays. Another login's row is not found.

func (*AgentService) UpdatePlaybook

UpdatePlaybook replaces playbooks/<name>.yaml.

func (*AgentService) UpdateProfile

UpdateProfile stores the active assistant definition and swaps the rebuilt profile in. The request is the whole definition. Playbooks stay files. A legacy override row is removed on the first save so the two cannot both apply.

func (*AgentService) UpdateProfileFile added in v0.3.2

UpdateProfileFile replaces profile.yaml and re-reads the profile directory. The old file is put back when the new one does not load, so a bad save leaves the conductor as it was.

func (*AgentService) UploadSkill

UploadSkill unpacks a zip or a bare SKILL.md into PODIUM_AGENT_SKILLS_DIR.

type AgentServiceOptions

type AgentServiceOptions struct {
	Store   *store.Store
	Secrets SecretStore
	// Model is the model reported by GetSettings when no profile is loaded. The profile's
	// own model wins when there is one, because it is what a turn will actually run.
	Model string
	// AnthropicBaseURL is where SetProviderKey validates an Anthropic key.
	AnthropicBaseURL string
	// XAIBaseURL is where it validates an xAI credential, key or token.
	XAIBaseURL string
	// XAIOAuthIssuer, XAIOAuthClientID and XAIOAuthScopes configure the subscription
	// sign-in. An empty client id means this control plane offers API keys only, which is
	// a supported configuration and not an error.
	XAIOAuthIssuer   string
	XAIOAuthClientID string
	XAIOAuthScopes   string
	// OpenAIBaseURL is where SetProviderKey validates an OpenAI API key.
	OpenAIBaseURL string
	// OpenAICodexBaseURL is where a ChatGPT / Codex subscription token is validated.
	OpenAICodexBaseURL string
	// OpenAIOAuthIssuer and OpenAIOAuthClientID configure the ChatGPT subscription
	// sign-in. An empty client id means API keys only.
	OpenAIOAuthIssuer   string
	OpenAIOAuthClientID string
	// AnthropicOAuth* configure the Claude subscription sign-in. An empty client id is API
	// keys only, and so is ClaudeAgent false: a Claude sign-in is only offered where the
	// chat host can run Claude Code, the one place it is spent.
	AnthropicOAuthClientID     string
	AnthropicOAuthAuthorizeURL string
	AnthropicOAuthTokenURL     string
	AnthropicOAuthRedirectURI  string
	AnthropicOAuthScopes       string
	ClaudeAgent                bool
	// GoogleBaseURL is where SetProviderKey validates a Gemini API key.
	GoogleBaseURL string
	// GitHubURL and GitHubAPIURL are where a person's GitHub account is connected. Empty
	// means github.com.
	GitHubURL    string
	GitHubAPIURL string
	// HTTPClient validates the key. Nil means a client with a timeout of its own.
	HTTPClient *http.Client
	// Memory is the shared-memory client. Nil is a supported configuration: the three
	// memory RPCs then answer FailedPrecondition and the UI says memory is not configured.
	Memory memory.Client
	// SkillsDir is PODIUM_AGENT_SKILLS_DIR: the Agent Skills on this host. Empty means this
	// conductor delivers no skills, and a playbook that names one fails that turn.
	SkillsDir string
	// Profiles is the profile in force, swapped when the directory is re-read or an override
	// changes. Nil makes the playbook and profile RPCs answer FailedPrecondition.
	Profiles *profiles.Live
	// ProfileDir is PODIUM_AGENT_PROFILE_DIR, the directory Profiles' file half was read
	// from. It is what ReloadProfileDir re-reads. Empty means this conductor has no
	// directory to go back to and that RPC answers FailedPrecondition.
	ProfileDir string
	// Chat is the web chat's write path and live fan-out. Nil makes the chat RPCs answer
	// FailedPrecondition.
	Chat ChatSource
	// Tasks stops a running Podium task. DeleteChat uses it so a conversation that still
	// has a turn in flight does not leave a container running after it is gone. Nil is a
	// supported configuration: tests that never start a turn omit it.
	Tasks TaskCanceller
	// Turns stops the work a conversation owns that no task id can reach: the host turn
	// answering it, which runs in this process, and the tasks that turn delegated. Nil is
	// a conductor with no host turns, and DeleteChat then has nothing extra to stop.
	Turns TurnStopper
	// Slack lists the channels the bot is in, so ListSlackChannels can seed the catalogue
	// before the first mention. Nil means this conductor has no Slack source.
	Slack  SlackDirectory
	Logger *slog.Logger
	// Env is the connection configuration the process was started with, before a saved
	// connection replaced it. Running is what this process actually opened. GetConnections
	// uses the two to say whether a restart is still owed. The zero value is an install
	// with nothing configured, which is what tests that never touch connections want.
	Env     config.Config
	Running config.Config
	// Connections switches the running conductor to a saved Slack pair or GitHub App, so a
	// save takes effect without a restart. Nil leaves the change for the next start, and
	// restart_required says so.
	Connections ConnectionApplier
}

AgentServiceOptions is what the handlers need. Everything but Store and Secrets is optional; without Secrets the settings RPCs answer FailedPrecondition rather than panic.

type ChatSource

type ChatSource interface {
	Send(ctx context.Context, req chat.SendRequest) (store.ChatMessage, error)
	Subscribe(ctx context.Context, chatID string) *chat.Subscriber
	Running(ctx context.Context, chatID string) (bool, error)
	Awaiting(chatID string) bool
	AttachPullRequest(ctx context.Context, chatID, login, url string) ([]store.ChatPullRequest, error)
	DetachPullRequest(ctx context.Context, chatID, login, url string) ([]store.ChatPullRequest, error)
}

ChatSource is the part of the chat source the handlers use: the write paths, and the live fan-out. Reads go to the store directly.

The pull-request writes are here rather than on the store because the point of them is the frame: every browser watching the conversation has to see the list change, not only the one that pressed the button.

type ConnectionApplier added in v0.7.1

type ConnectionApplier interface {
	// Check tries the Slack pair and GitHub App in cfg against Slack and GitHub, where they
	// differ from what is running. It is how a mistyped token is refused at save rather
	// than found by the conductor failing to connect with it.
	Check(ctx context.Context, cfg config.Config) error
	// Apply rereads the saved connections and swaps in whatever changed. Turns in flight
	// keep running. On success it calls SetRunning with what it switched to.
	Apply(ctx context.Context) error
}

ConnectionApplier is the part of the conductor that holds Slack and GitHub open.

type Delegator

type Delegator interface {
	Delegate(ctx context.Context, token, playbook, instruction string, opts conductor.DelegateOptions) (store.Delegation, error)
	GetDelegation(ctx context.Context, token, id string) (store.Delegation, string, error)
	Delegations(ctx context.Context, token string) ([]store.Delegation, error)
	CancelDelegation(ctx context.Context, token, id, reason string) (store.Delegation, error)
	InjectDelegation(ctx context.Context, token, id, text string) (store.Delegation, error)
	ReleaseDelegationPreview(ctx context.Context, token, id string) (store.Delegation, error)
}

Delegator is the conductor, as this service needs it. An interface rather than the concrete type so the handler can be tested without a turn loop, a database or a control plane behind it.

type DevSource

type DevSource struct {
	*fakesource.Source
}

DevSource is the in-memory source with two HTTP routes bolted on. It is TEST ONLY: it lets a test inject an inbound message as if a human had sent it and read back every single thing the conductor said, in order, without a Slack workspace.

func NewDevSource

func NewDevSource() *DevSource

NewDevSource returns a started-and-ready dev source.

func (*DevSource) Handler

func (d *DevSource) Handler() http.Handler

Handler mounts the two dev routes.

type GitCredentialService

type GitCredentialService struct {
	// contains filtered or unexported fields
}

GitCredentialService implements podium.agent.v1.GitCredentialService.

func NewGitCredentialService

func NewGitCredentialService(minter GitMinter, logger *slog.Logger) *GitCredentialService

NewGitCredentialService returns the handler. A nil minter is a conductor with no GitHub App: every call then answers FailedPrecondition, which is the truth rather than a crash.

func (*GitCredentialService) MintToken

MintToken issues a GitHub installation token for the calling turn.

type GitMinter

type GitMinter interface {
	MintGitToken(ctx context.Context, capability string) (conductor.GitCredential, error)
}

GitMinter is the conductor, as this service needs it. An interface so the handler can be tested without a GitHub App, a database or a turn loop behind it.

type SecretStore

type SecretStore interface {
	SetSecret(ctx context.Context, name string, value []byte) (version int32, err error)
	DeleteSecret(ctx context.Context, name string) error
	SecretVersion(ctx context.Context, name string) (int32, error)
	// SetPersonalSecret stores a secret owned by one login. The conductor uses it for
	// the token a person saved on their own MCP server, so the secret shows on their
	// list and not the company list.
	SetPersonalSecret(ctx context.Context, owner, name string, value []byte) (version int32, err error)
	DeletePersonalSecret(ctx context.Context, owner, name string) error
	// PersonalSecretVersion is the version of one person's secret, or 0 when it is absent.
	PersonalSecretVersion(ctx context.Context, owner, name string) (int32, error)
	// ReadSecret reads back one of the names the conductor keeps for itself, such as a
	// saved connection's credentials. See spec.ConductorMayRead.
	ReadSecret(ctx context.Context, owner, name string) ([]byte, error)
}

SecretStore is the part of the Podium API the settings handlers need. It is an interface rather than the client itself so a test needs no control plane, and so this package keeps depending on nothing but a name, some bytes and a version.

SecretVersion is what keeps the conductor honest. The secret belongs to the control plane and an operator can set or remove it with `podium secret set`/`rm` without the conductor hearing about it, so the metadata row here is never taken as proof that a key exists — the control plane is asked. ListSecrets carries metadata only; no value is ever read back.

type SlackDirectory added in v0.2.0

type SlackDirectory interface {
	ListChannels(ctx context.Context) ([]slack.ChannelInfo, error)
}

SlackDirectory is the Slack source as the Channels screen is allowed to see it: the conversations the bot is in, so the catalogue can be seeded before the first mention.

type TaskCanceller

type TaskCanceller interface {
	CancelTask(ctx context.Context, taskID, reason string) error
}

TaskCanceller stops a Podium task. It is the same CancelTask `podium task cancel` calls: the node gets SIGTERM and up to 30s, and nothing here waits.

type TurnService

type TurnService struct {
	// contains filtered or unexported fields
}

TurnService implements podium.agent.v1.TurnService.

func NewTurnService

func NewTurnService(delegator Delegator, logger *slog.Logger) *TurnService

NewTurnService returns the handler. A nil delegator is a conductor that runs no host turns: every call then answers FailedPrecondition, which is the truth rather than a crash.

func (*TurnService) CancelDelegation

CancelDelegation stops one.

func (*TurnService) Delegate

Delegate starts a task for the calling turn's conversation.

func (*TurnService) GetDelegation

GetDelegation is where a delegated task has got to. It is what a turn polls, so it is deliberately cheap: one row and the last thing the task said.

func (*TurnService) InjectDelegation

InjectDelegation delivers one human message into a running delegated task.

func (*TurnService) ListDelegations

ListDelegations is everything the calling turn has delegated.

func (*TurnService) ReleasePreview added in v0.6.0

ReleasePreview takes down the preview a finished delegated task left up.

type TurnStopper

type TurnStopper interface {
	// CancelHostTurn stops the host turn answering ref, reporting whether there was one.
	CancelHostTurn(ref string) bool
	// CancelDelegationsForRef stops every delegated task of one conversation.
	CancelDelegationsForRef(ctx context.Context, ref, reason string) (int, error)
}

TurnStopper is the conductor, as deleting a chat needs it. Neither of these can be reached with a task id: a host turn is a child process of this conductor and has no task, and the tasks it delegated are owned by the CONVERSATION rather than by the turn that asked for them — which is precisely why deleting the conversation has to end them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL