api

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 42 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 (
	AuditRoutineCreate = "routine.create"
	AuditRoutineUpdate = "routine.update"
	AuditRoutineDelete = "routine.delete"
	AuditRoutinePause  = "routine.pause"
	AuditRoutineResume = "routine.resume"
	AuditRoutineRun    = "routine.run"
	AuditRoutineCancel = "routine.cancel"
	AuditRoutineLimits = "routine.limits"
)

Audit actions on routines. The control plane takes these from the conductor (audit.go).

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 (
	AuditChatShare         = "chat.share"
	AuditChatUnshare       = "chat.unshare"
	AuditChatGeneralAccess = "chat.general_access"
	AuditChatLinkReset     = "chat.link_reset"
	AuditChatJoin          = "chat.join"
	AuditChatLeave         = "chat.leave"
	AuditChatDelete        = "chat.delete"
)

Audit actions. They are all "chat." so the control plane can refuse anything else from the conductor's credential.

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 GitHubReadMCPRESTPath = "/v1/mcp/github-read"

GitHubReadMCPRESTPath is the GitHub reader a routine's turn gets (conductor/githubread.go).

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 RecordWebIdentities added in v1.0.0

func RecordWebIdentities(st WebIdentityStore, logger *slog.Logger, next http.Handler) http.Handler

RecordWebIdentities records every login podium-server proxies a call for as a web identity: the row that means "this person is in Podium". A Slack email or a GitHub account is only ever matched to a login that has one, so a person becomes resolvable on other surfaces by signing in here once.

It must sit INSIDE RequireBearer: the login it trusts is the one RequireBearer took from the proxy's header. The dev token is skipped — it is a shared operator credential, not a person — as are a call that carried no login and the transport's own non-person names (mcpPersonalOwner's list).

A failed write is logged and the request goes on: an identity that is recorded on the next call is a much smaller problem than a UI that stops working when the table is unhappy.

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) CancelChatTurn added in v1.0.0

CancelChatTurn stops the turn answering a chat, for anybody who may write it. Who started the turn does not matter: in a shared conversation, stopping an answer that is going wrong is everybody's to do, and the person whose turn it was can ask again.

func (*AgentService) CancelQueuedTurn added in v1.0.0

CancelQueuedTurn takes back the caller's own waiting turn.

func (*AgentService) CancelRoutineRun added in v1.0.0

CancelRoutineRun stops the run going now in one of the caller's routines. It is how an admin stops a system routine's run, whose chat nobody may write in.

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) CreateRoutine added in v1.0.0

CreateRoutine opens a routine and its chat for the caller.

func (*AgentService) CreateRoutineFor added in v1.0.0

func (s *AgentService) CreateRoutineFor(ctx context.Context, owner string, d store.RoutineDraft) (store.Routine, error)

CreateRoutineFor opens a routine for owner that an assistant set up in a conversation at their request (the create_routine tool). It is checked exactly as the person's own would be: the same schedule floor, the same count, the same assistants. The error's message is the reason alone, for the assistant to tell the person.

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) DeleteRoutine added in v1.0.0

DeleteRoutine removes one of the caller's routines. Its chat stays.

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) GetChatSharing added in v1.0.0

GetChatSharing is the share dialog, for anybody who may open the chat.

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) GetRoutine added in v1.0.0

GetRoutine is one of the caller's routines, by id or by its chat.

func (*AgentService) GetRoutineLimits added in v1.0.0

GetRoutineLimits is the per-person limits, for anyone: a person may want to know theirs.

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) LeaveChat added in v1.0.0

LeaveChat takes the caller out of somebody else's chat.

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) ListMemberRoutines added in v1.0.0

ListMemberRoutines is every person's routines as an admin sees them: metadata only.

func (*AgentService) ListMemories

ListMemories pages through the shared memory, newest first.

func (*AgentService) ListNotifications added in v1.0.0

ListNotifications is the caller's inbox.

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) ListRoutineRevisions added in v1.0.0

ListRoutineRevisions is the history of what one of the caller's routines was told to do.

func (*AgentService) ListRoutineRuns added in v1.0.0

ListRoutineRuns pages one of the caller's routine's runs, newest first.

func (*AgentService) ListRoutines added in v1.0.0

ListRoutines is the caller's routines; every routine for the dev token.

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) MarkChatRead added in v1.0.0

MarkChatRead moves the caller's read mark forward.

func (*AgentService) MarkNotificationsRead added in v1.0.0

MarkNotificationsRead marks some or all of the caller's notifications read.

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) PauseRoutine added in v1.0.0

PauseRoutine stops a routine running until it is resumed.

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) ResetRoutineState added in v1.0.0

ResetRoutineState empties what the agent kept across runs.

func (*AgentService) ResolveRoutineNote added in v1.0.0

ResolveRoutineNote accepts the proposed note into the notes, or dismisses it.

func (*AgentService) ResumeRoutine added in v1.0.0

ResumeRoutine starts a paused routine again from the next time its schedule names. The runs it missed while paused are not made up.

func (*AgentService) RetryChatDelivery added in v1.0.0

RetryChatDelivery puts one bot message's failed deliveries back in the queue. Anybody who can read the chat may press it: they are the people who can see that it did not arrive, and retrying only re-sends what the bot already said where it was meant to say it. Phase 5 narrows this to editors.

func (*AgentService) RunRoutineNow added in v1.0.0

RunRoutineNow starts a run at once.

func (*AgentService) SearchMembers added in v1.0.0

SearchMembers lists Podium people for the composer's @-mention list.

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) SetRoutineLimits added in v1.0.0

SetRoutineLimits sets the per-person limits. Admins and owners only.

func (*AgentService) SetRoutines added in v1.0.0

func (s *AgentService) SetRoutines(runner RoutineRunner)

SetRoutines hands the service the scheduler, which is built after it.

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) ShareChat added in v1.0.0

ShareChat grants a Podium person a role. Owner only.

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) UnshareChat added in v1.0.0

UnshareChat removes a named grant. Owner only. Somebody who has spoken in the chat stays a participant: unsharing takes back what the owner handed out, not a seat in a conversation.

func (*AgentService) UpdateChatSharing added in v1.0.0

UpdateChatSharing changes general access, its role, and the link. Owner only.

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) UpdateRoutine added in v1.0.0

UpdateRoutine replaces what the caller edits on one of their routines.

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
	// Deliveries retries a bot answer's failed deliveries to Slack or GitHub. Nil turns
	// RetryChatDelivery off (Unimplemented).
	Deliveries DeliveryRetrier
	// Canceller stops the turn answering a chat (CancelChatTurn). Nil refuses that RPC.
	Canceller ChatTurnCanceller
	// Auditor writes sharing decisions to the control plane's audit_log. Nil writes nothing.
	Auditor ChatAuditor
	// 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 ChatAuditor added in v1.0.0

type ChatAuditor interface {
	AuditChat(ctx context.Context, actor, action, chatID string, details map[string]any) error
}

ChatAuditor writes the sharing decisions somebody made to the control plane's audit_log (podium-server), which is where every other "who did what" already lives. Optional: a conductor without a control plane, or a test, leaves it nil and nothing is written.

type ChatSource

type ChatSource interface {
	SendMessage(ctx context.Context, req chat.SendRequest) (chat.SendResult, error)
	// QueueChanged redraws a chat's queue on every screen showing it.
	QueueChanged(chatID string)
	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)
	// Multiplayer marks a chat multiplayer, force for a share; ChatChanged shows a chat's row
	// to every screen watching it after its sharing changed (phase 5).
	Multiplayer(ctx context.Context, chatID string, force bool, who string)
	ChatChanged(ctx context.Context, chatID string)
}

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 ChatTurnCanceller added in v1.0.0

type ChatTurnCanceller interface {
	CancelChatTurn(ctx context.Context, chatID string) (bool, error)
}

ChatTurnCanceller stops the turn answering a chat right now, on whichever conductor runs it (phase 4). The conductor implements it.

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)
	StartChat(ctx context.Context, token string, req conductor.StartChatRequest) (conductor.StartedChat, error)
	FinishRoutine(ctx context.Context, token string, req conductor.RoutineFinishRequest) (string, error)
	CreateRoutine(ctx context.Context, token string, req conductor.CreateRoutineRequest) (string, 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 DeliveryRetrier added in v1.0.0

type DeliveryRetrier interface {
	RetryDelivery(ctx context.Context, chatID string, seq uint64) error
}

DeliveryRetrier puts a bot answer's failed deliveries back in the outbox and attempts them. The conductor implements it; ErrNothingToRetry-style refusals come back as errors the handler maps to NotFound.

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 RoutineRunner added in v1.0.0

type RoutineRunner interface {
	RunNow(ctx context.Context, routineID string) (store.RoutineRun, error)
}

RoutineRunner starts a run outside a routine's schedule. The scheduler implements 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) CreateRoutine added in v1.0.0

CreateRoutine opens a routine for the person who asked for it in the calling turn's chat.

func (*TurnService) Delegate

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

func (*TurnService) FinishRoutine added in v1.0.0

FinishRoutine ends the calling turn's routine run.

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.

func (*TurnService) StartChat added in v1.0.0

StartChat opens a new chat for the calling turn's person and returns the link to it.

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.

type WebIdentityStore added in v1.0.0

type WebIdentityStore interface {
	UpsertIdentity(ctx context.Context, id store.Identity) error
}

WebIdentityStore is what RecordWebIdentities writes to.

Jump to

Keyboard shortcuts

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