askpass

package
v1.59.0 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package askpass bridges the questions ssh and git ask into the browser. The server points SSH_ASKPASS and GIT_ASKPASS of one user-triggered action at a helper that reports the prompt line here and blocks; the standing questions are server state, addressed by the project whose action asked, so any signed-in page shows the question, the typed answer travels back to the helper, which prints it and lets the action continue.

The rules the whole package is built around:

  • only an action somebody started gets a bridge: nothing here is wired into a call unless the handler opened an Action for it, and everything else keeps failing fast against /bin/false.
  • an answer lives in memory and only for its one question: it is never logged, never written anywhere, and handed out exactly once.
  • the helper authenticates with a one-time token from its environment; the browser side needs nothing beyond the session, because a question belongs to the cockpit and not to the page that started the action: the page may be reloaded, updated away or lying on a desk while the phone answers.
  • an ended action unblocks every waiting helper with a denial, so cancelling never leaves a hanging process behind.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Ask

func Ask(socket, token, prompt string) (string, error)

Ask is the helper's one move: report the prompt over the broker's socket and block until the person answered, was denied, or the action ended. It is what the hidden askpass command runs, and what a test stands in for a real helper with.

func Listen

func Listen(stateDir string) (net.Listener, error)

Listen opens the broker's socket the way the local API opens its own.

func SocketPath

func SocketPath(stateDir string) string

SocketPath answers where the broker of a state directory listens. The directory is resolved first (filesystem.AbsDir, the same rule the local API's socket follows), and that is not cosmetic here: this path travels into a git process whose working directory is the project, so a relative one would be read against the wrong directory and every helper would miss the socket without saying why. It is also what the length below is measured on, so a short spelling of a long path cannot walk past the fallback into an address the kernel refuses.

func WriteScript

func WriteScript(stateDir string) (string, error)

WriteScript puts the helper stub next to the socket: ssh executes SSH_ASKPASS without arguments of ours, so a two line script hands over to this binary's hidden askpass command. It is rewritten on every start, because the binary's path is baked in and a self update moves it. The directory is its own to make, with the permission the socket's carries: in which order a start opens the socket and writes the stub is not a rule this may depend on.

Types

type Action

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

Action is one user-triggered git call's bridge. The helper side finds it by the one-time token, the browser side by the project it runs in: the write lock lets one write per working copy through, so a project never runs two.

func (*Action) Answer

func (a *Action) Answer(id, text string, deny bool) bool

Answer resolves the pending question. deny is the cancel button: the helper fails, the action ends in git's words, and Cancelled remembers why.

func (*Action) Answered

func (a *Action) Answered() <-chan struct{}

func (*Action) Asked

func (a *Action) Asked() <-chan struct{}

Asked signals every question that arrives; Answered every answer that went back. The caller's watchdog stretches its deadline on them: a person gets their own time, and an answered action gets its full budget back.

func (*Action) Cancelled

func (a *Action) Cancelled() bool

Cancelled reports whether somebody pressed cancel on a question of this action, which is what turns the refusal message into its honest sentence.

func (*Action) End

func (a *Action) End()

End closes the bridge: the maps forget the action and every helper still waiting is denied, so nothing blocks past the action it belonged to. It is the one call every handler defers, on paths that may already have ended the action themselves, so ending twice has to be the same as ending once rather than a panic on a closed channel.

func (*Action) Env

func (a *Action) Env() []string

Env is what the spawned git call carries so its helpers can call home.

func (*Action) Name

func (a *Action) Name() string

Name answers what the action is called, the word the dialog and the notification carry as this server's truth ("push", "pull").

func (*Action) Question

func (a *Action) Question() *Question

Question is the pending question of this one action, or nothing.

type Broker

type Broker struct {

	// OnChange, when set, is called whenever the standing questions moved: one
	// was parked, answered, or taken along by its action's end. The server
	// hangs the gitprompt event on it. It is set once at wiring time, before
	// any action exists.
	OnChange func()
	// contains filtered or unexported fields
}

Broker owns the socket the helpers call and the actions currently allowed to ask. Everything lives in memory and dies with the action.

func New

func New(stateDir string) *Broker

New builds a broker for the state directory's socket path. Serve is the caller's, like the local API: Listen, then http.Serve with Handler.

func (*Broker) Begin

func (b *Broker) Begin(project, action string) *Action

Begin opens the bridge for one action of one project. A project that already runs one is refused: the write lock in front of every bridge makes that unreachable, so this is an invariant guard and not a surface, but a second action under the same name would answer questions to the wrong caller and is the one thing this may never do silently.

func (*Broker) BeginCommand

func (b *Broker) BeginCommand(project, action, command, dir string) *Action

BeginCommand is Begin for a caller outside the app: its questions are the ones that have to leave the app to be seen, and the command line and the working copy travel with them for the dialog to show. Everything else is Begin's, including the one action per project rule.

func (*Broker) Find

func (b *Broker) Find(project string) *Action

Find answers the browser's side of a project's running action.

func (*Broker) Handler

func (b *Broker) Handler() http.Handler

Handler is the broker's whole HTTP surface: one endpoint the helper posts its prompt to and blocks on. Nothing here is ever logged, an answer only exists in the response body.

func (*Broker) Questions

func (b *Broker) Questions() []Question

Questions answers the standing questions of every running action, oldest first, which is the order the global dialog serves them in. Usually it is empty or holds one; two mean two devices started two actions in two projects and both hit a prompt.

type Question

type Question struct {
	ID       string `json:"id"`
	Project  string `json:"project"`
	Action   string `json:"action"`
	Prompt   string `json:"prompt"`
	External bool   `json:"external,omitempty"`
	Command  string `json:"command,omitempty"`
	Dir      string `json:"dir,omitempty"`
}

Question is what the browser shows: the prompt line ssh or git wrote, an id that makes sure an answer meets its own question, and the two values that say whose question it is. Project and Action are this server's own truth from the handler that opened the bridge, while the prompt line is written by ssh, git, or whatever program the repository put in their way; the dialog keeps them visually apart for exactly that reason, and leaves the line out where Command below already says the same in more detail.

External separates the two kinds of asker, and it is the whole policy in one field: an action somebody started in the app is a page they are looking at, while a proxied call (`dev-cockpit git`) was typed in a terminal that cannot answer anything, by a caller who may be a coding agent. Only an external question has to leave the app to be seen at all, so only that one becomes a notification and rides the push channels; ringing for a dialog the person is already in front of is the one thing this may not do.

It is its own field and not read off Command, which is the text below and would carry the policy on a rendering detail: the day a second surface in the app wants to show what it is about to run, setting Command would turn the push channels on for it.

Command is what the dialog shows a caller nobody in the browser can see: the command line as it was typed, because somebody answering a passphrase for them has to be able to read what is about to run. Dir rides along and is the working copy it runs in, which is half of what somebody has to recognise: the same `git push` means different things in two checkouts of one repository, and the caller picked its project through a working directory nobody in the browser can see.

Jump to

Keyboard shortcuts

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