Documentation
¶
Index ¶
- Constants
- Variables
- func AttachmentsRootForTask(dataHome, taskID string) string
- func BuildFlatItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem
- func BuildTreeItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem
- func EnsureAttachmentsDir(dataHome, taskID string) (string, error)
- func ListAttachments(dataHome, taskID string) ([]string, error)
- func NormalizePublicURL(raw string) (string, error)
- func ReadAttachment(dataHome, taskID, name string) ([]byte, error)
- func ResolveJSONField(raw json.RawMessage, path string) (string, error)
- func ResolveTaskField(task *orchestrator.Task, actions orchestrator.LifecycleStore, path string) (string, error)
- func SanitizeAttachmentName(raw string) (string, error)
- func SaveMultipartAttachments(dataHome, taskID string, files []*multipart.FileHeader) ([]string, error)
- func ValidateAttachmentHeaders(files []*multipart.FileHeader) error
- type ActionApplication
- type ActionHandler
- type ActionStore
- type AnswerTaskRequest
- type ApplyActionRequest
- type BlockingAskRegistry
- func (r *BlockingAskRegistry) Cancel(qid string)
- func (r *BlockingAskRegistry) Has(qid string) bool
- func (r *BlockingAskRegistry) Notify(qid, answer string) bool
- func (r *BlockingAskRegistry) Register(taskID, qid string) error
- func (r *BlockingAskRegistry) Wait(ctx context.Context, qid string) (string, error)
- type BrokerHandler
- type BrokerRegisterRequest
- type BrokerRegisterResponse
- type BrokerRegistry
- type ConfigHandler
- type ConfigService
- type CreateProjectRequest
- type CreateTaskRequest
- type DeviceAuthHandler
- type DeviceGCStore
- type DispatchCoordinator
- type DuplicateTaskRequest
- type ExecDispatcher
- type GCAppService
- type GCHandler
- type GCService
- type GCStore
- type GlobalJobStore
- type HookService
- type HostCommandsHandler
- type HostCommandsProvider
- type HostCommandsService
- type ImportError
- type ImportResult
- type Job
- type JobCompletion
- type JobDoneRequest
- type JobHandler
- func (h *JobHandler) AgentStop(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) Done(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) Log(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) Patch(w http.ResponseWriter, r *http.Request)
- func (h *JobHandler) Routes() chi.Router
- type JobLifecycle
- type JobListFilter
- type JobLogReader
- type JobLogSSEHandler
- type JobStatus
- type JobStore
- type JobWithContext
- type LoginHandler
- type MetaStore
- type Notifier
- type NotifyTaskRequest
- type Pairer
- type ProjectAppService
- func (s *ProjectAppService) CreateProject(workDir string) (*orchestrator.Project, error)
- func (s *ProjectAppService) CreateWorkspace(slug string, meta *orchestrator.WorkspaceMeta) (*WorkspaceDetail, error)
- func (s *ProjectAppService) DeleteProject(id string) error
- func (s *ProjectAppService) ExportWorkspace(slug string) ([]byte, string, error)
- func (s *ProjectAppService) GetProject(id string) (*orchestrator.Project, error)
- func (s *ProjectAppService) GetWorkspace(slug string) (*WorkspaceDetail, error)
- func (s *ProjectAppService) ImportWorkspace(slug string, meta *orchestrator.WorkspaceMeta, mode string) (*WorkspaceDetail, error)
- func (s *ProjectAppService) ListProjects(workspaceID string) ([]*orchestrator.Project, error)
- func (s *ProjectAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
- func (s *ProjectAppService) ReloadProjects() (*ProjectReloadResult, error)
- func (s *ProjectAppService) RemoveWorkspace(slug string) error
- func (s *ProjectAppService) ResolveProjectRef(ref string) ([]*orchestrator.Project, error)
- func (s *ProjectAppService) SetProjectWorkspace(id, workspaceID string) (*orchestrator.Project, error)
- func (s *ProjectAppService) UpdateWorkspace(slug string, meta *orchestrator.WorkspaceMeta, ifMatch string, force bool) (*WorkspaceDetail, error)
- type ProjectHandler
- func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) Reload(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) Routes() chi.Router
- func (h *ProjectHandler) SetWorkspace(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) StartExec(w http.ResponseWriter, r *http.Request)
- func (h *ProjectHandler) StartSession(w http.ResponseWriter, r *http.Request)
- type ProjectReloadResult
- type ProjectRepository
- type ProjectService
- type ProjectWorkDirLookup
- type ReopenTaskRequest
- type ReplayHookRequest
- type ReplayHookResult
- type RerunTaskRequest
- type SecretHandler
- func (h *SecretHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *SecretHandler) GetValue(w http.ResponseWriter, r *http.Request)
- func (h *SecretHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *SecretHandler) Routes() chi.Router
- func (h *SecretHandler) Set(w http.ResponseWriter, r *http.Request)
- type SecretStore
- type SessionDispatcher
- type SessionHandler
- type SetProjectWorkspaceRequest
- type StartExecRequest
- type StartExecResult
- type StartSessionRequest
- type StartSessionResult
- type StatusError
- type TaskAnswerService
- type TaskAppService
- func (s *TaskAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error
- func (s *TaskAppService) AskTaskBlocking(ctx context.Context, taskID, question string) (string, error)
- func (s *TaskAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
- func (s *TaskAppService) DeleteTask(id string, force bool) error
- func (s *TaskAppService) DuplicateTask(sourceID string, autoStart bool) (*orchestrator.Task, error)
- func (s *TaskAppService) GetInstructions(id string) ([]orchestrator.RoutedInstruction, error)
- func (s *TaskAppService) GetInstructionsField(id, path string) (string, error)
- func (s *TaskAppService) GetTask(id string) (*orchestrator.Task, error)
- func (s *TaskAppService) GetTaskCurrent(id string) (*orchestrator.TaskSnapshot, error)
- func (s *TaskAppService) GetTaskCurrentField(id, path string) (string, error)
- func (s *TaskAppService) GetTaskDetail(id string) (*TaskDetailView, error)
- func (s *TaskAppService) GetTaskField(id, path string) (string, error)
- func (s *TaskAppService) ImportTasks(reqs []CreateTaskRequest) (*ImportResult, error)
- func (s *TaskAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
- func (s *TaskAppService) NotifyTask(ctx context.Context, ...) error
- func (s *TaskAppService) RerunTask(id string, req RerunTaskRequest) (*orchestrator.Task, error)
- func (s *TaskAppService) UpdateTask(id string, req UpdateTaskRequest) (*orchestrator.Task, error)
- func (s *TaskAppService) UpdateTaskPayloadPatch(jobID string, patch json.RawMessage, allowedTraits []orchestrator.TraitType) (*orchestrator.Task, error)
- type TaskDetailView
- type TaskEvent
- type TaskEventHub
- type TaskHandler
- func (h *TaskHandler) Answer(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Detail(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Duplicate(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Field(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Import(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) ListHooks(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Notify(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Patch(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) ReplayHook(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Rerun(w http.ResponseWriter, r *http.Request)
- func (h *TaskHandler) Routes() chi.Router
- type TaskNotifyService
- type TaskService
- type TaskStore
- type TaskWorkflowService
- func (s *TaskWorkflowService) ApplyAction(ctx context.Context, taskID string, req ApplyActionRequest) (*ActionApplication, error)
- func (s *TaskWorkflowService) CompleteJob(_ context.Context, jobID string, req JobDoneRequest) (*Job, error)
- func (s *TaskWorkflowService) InitDispatch(ctx context.Context)
- func (s *TaskWorkflowService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
- func (s *TaskWorkflowService) ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
- func (s *TaskWorkflowService) Shutdown()
- func (s *TaskWorkflowService) StopAgent(runtimeID string)
- type Transactor
- type TxStore
- type UpdateJobRequest
- type UpdateTaskRequest
- type WSAttachHandler
- type WebAppService
- func (s *WebAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error
- func (s *WebAppService) ApplyAction(taskID string, actionType string) error
- func (s *WebAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
- func (s *WebAppService) DeleteTask(id string, force bool) error
- func (s *WebAppService) DuplicateTask(id string) (string, error)
- func (s *WebAppService) GetJob(id string) (*JobWithContext, error)
- func (s *WebAppService) GetProjectByID(id string) (*orchestrator.Project, error)
- func (s *WebAppService) GetTaskDetail(id string) (*TaskDetailView, error)
- func (s *WebAppService) ListBehaviors() ([]string, error)
- func (s *WebAppService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
- func (s *WebAppService) ListJobs(status string) ([]JobWithContext, error)
- func (s *WebAppService) ListProjects() ([]*orchestrator.Project, error)
- func (s *WebAppService) ListSessions() ([]JobWithContext, error)
- func (s *WebAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
- func (s *WebAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
- func (s *WebAppService) ReopenTask(id string, req ReopenTaskRequest) error
- func (s *WebAppService) ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
- func (s *WebAppService) RerunTask(id string, req RerunTaskRequest) error
- func (s *WebAppService) UpdateTask(id string, req UpdateTaskRequest) error
- type WebHandler
- func (h *WebHandler) GetTaskEdit(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) HookReplayList(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) JobDetail(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) JobTerminal(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostAction(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostAnswer(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostDelete(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostDuplicate(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostEdit(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostHookReplay(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostReopen(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostRerun(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostStartSession(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) PostTaskCreate(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) QuestionPage(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) ReopenForm(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) Routes() chi.Router
- func (h *WebHandler) SessionList(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) SessionNew(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) TaskDetail(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) TaskDetailFragment(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) TaskEvents(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) TaskList(w http.ResponseWriter, r *http.Request)
- func (h *WebHandler) TaskNew(w http.ResponseWriter, r *http.Request)
- type WebManagementHandler
- func (h *WebManagementHandler) DeleteAllDevices(w http.ResponseWriter, r *http.Request)
- func (h *WebManagementHandler) DeleteDevice(w http.ResponseWriter, r *http.Request)
- func (h *WebManagementHandler) GetDevices(w http.ResponseWriter, r *http.Request)
- func (h *WebManagementHandler) PostPair(w http.ResponseWriter, r *http.Request)
- func (h *WebManagementHandler) Routes() chi.Router
- type WebService
- type WorkflowService
- type WorkspaceDetail
- type WorkspaceHandler
- func (h *WorkspaceHandler) Create(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) Export(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) Import(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) List(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) Remove(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) Routes() chi.Router
- func (h *WorkspaceHandler) Show(w http.ResponseWriter, r *http.Request)
- func (h *WorkspaceHandler) Update(w http.ResponseWriter, r *http.Request)
- type WorkspaceHomeSize
- type WorkspaceRemoveResponse
- type WorkspaceSlugLister
- type WorkspaceStore
Constants ¶
const ( AttachmentMaxFileBytes int64 = 10 * 1024 * 1024 // 10 MB per file AttachmentMaxTotalBytes int64 = 30 * 1024 * 1024 // 30 MB per task dir AttachmentMaxNameBytes = 255 )
Attachment-related constants. Values are also enforced client-side in web/static/boid-paste-attach.js; keep the two in sync.
Variables ¶
var AttachmentAllowedExts = map[string]bool{ ".png": true, ".jpg": true, ".jpeg": true, ".webp": true, ".txt": true, ".md": true, ".json": true, ".log": true, }
AttachmentAllowedExts is the file-extension allowlist for uploads. Both images (for screenshot paste) and a few text formats (for logs / json snippets) are accepted.
var ErrAskPending = errors.New("task_ask: another question is pending")
ErrAskPending is returned by BlockingAskRegistry.Register when the task already has an outstanding blocking ask. It implements decision B1: a second concurrent `boid task ask` for the same task fails immediately rather than queueing behind the first.
Functions ¶
func AttachmentsRootForTask ¶ added in v0.0.7
AttachmentsRootForTask resolves the on-disk attachments directory for the given task. dataHome is the data root (e.g. filepath.Dir(cfg.DBPath)), the same convention as runtimesDirFor.
taskID must be a single canonical path component (codex review on PR #798, Phase 5b PR2 attachments RPCs): CreateTaskRequest.ID is caller-supplied and saved as the literal DB primary key without validation (internal/api/task_create.go), and the broker (internal/sandbox/broker.go) authorizes attachments ops by comparing that *raw* TaskID string against the token's own context — never a resolved filesystem path. Without this guard, a task literally IDed "alias/../<victim-id>" would pass the broker's string-equality check trivially (both sides carry the identical literal alias) while filepath.Join here silently collapsed it down to the *victim's* real attachments directory — a cross-task leak. Rejecting a non-canonical taskID here (returning "", the same fail-closed sentinel already used for an empty dataHome/taskID) protects every caller uniformly: the write path (EnsureAttachmentsDir, SaveMultipartAttachments) and the read path (ListAttachments, ReadAttachment) all resolve through this one function.
func BuildFlatItems ¶
func BuildFlatItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem
BuildFlatItems returns tasks as a flat list (Depth=0, HasChildren=false, ParentID=""). Used for the "closed" status view where tree structure is irrelevant.
func BuildTreeItems ¶
func BuildTreeItems(tasks []*orchestrator.Task, projectNames map[string]string) []components.TreeItem
BuildTreeItems takes a flat task list and returns items in DFS tree order with depth and visual-parent info. Siblings appear in input order. Tasks whose ParentID is absent from the list are treated as roots. Cycles in ParentID references are detected via a visited set and skipped.
projectNames maps project ID to display name; pass nil to skip name resolution.
func EnsureAttachmentsDir ¶ added in v0.0.7
EnsureAttachmentsDir creates the per-task attachments directory if it does not already exist. Used by handlers right after task creation / before accepting answer attachments so the bind mount has a non-empty source.
func ListAttachments ¶ added in v0.0.13
ListAttachments returns the basenames of the regular files directly under the task's attachments directory, sorted. Subdirectories are never recursed into — attachments are a flat namespace. A task that has never received an attachment (missing directory) returns an empty, non-nil slice rather than an error, matching the RPC's "no data" convention (JSON `[]`, not `null`).
Every symlink is excluded outright, regardless of where it points — codex review on PR #798 (Nit) flagged that the original version only filtered *escaping* symlinks, which could advertise a name ReadAttachment (which now rejects every symlink categorically, via openat2 RESOLVE_NO_SYMLINKS on Linux — see the Major/TOCTOU fix) would then always 404 on. Non-regular entries (FIFOs, sockets, devices — never created by SaveMultipartAttachments, but not something a shared filesystem rules out) are excluded for the same reason: list's admission criteria must exactly match what get can actually serve.
func NormalizePublicURL ¶ added in v0.0.13
NormalizePublicURL parses raw as an HTTPS origin, strips path/query/fragment, lowercases and IPv6-brackets the host, and returns the canonical `https://host[:port]` form. It exists so wire.go can validate cfg.Web.PublicURL once at daemon startup and hand the canonical form to DeviceAuthHandler.PublicURL — the value that ends up in the POST /api/auth/device response, which the CLI (PR2) will byte-compare against its saved profile URL on every subsequent request. An un-normalized value (path, trailing slash, `HOST.EXAMPLE.COM`, plain http, empty host like `https://:443`) would either fail the CLI's origin-bind check later or, worse, get accepted and silently misroute future requests.
This is also the exact same normalization applied to the request Host header fallback inside DeviceAuthHandler.canonicalURL — see the code path there for the "fallback must not bypass this validator" rule.
Empty raw is not an error (PublicURL is optional at boot — the handler falls back to the request's Host header). A non-empty raw that fails validation is returned as an error so the operator sees the misconfig at startup instead of at the first pair attempt.
func ReadAttachment ¶ added in v0.0.13
ReadAttachment returns the bytes of one attachment, addressed by its exact basename. name must be a single canonical path component: a traversal or absolute-path attempt ("../../etc/passwd", "/etc/passwd") is rejected by validateAttachmentLookupName before any path is constructed, rather than being silently coerced into some *other* file the way SanitizeAttachmentName's upload-time "pick a safe name to store under" contract does. The actual open+read (readAttachmentBytes, platform- specific — see attachment_read_linux.go) opens the file exactly once and reuses that descriptor for both the containment/type check and the capped read, closing the TOCTOU window a separate check-then-reopen sequence would leave (codex review on PR #798).
func ResolveJSONField ¶ added in v0.0.13
func ResolveJSONField(raw json.RawMessage, path string) (string, error)
ResolveJSONField resolves a dotted path against arbitrary already-marshaled JSON data, using the same segment-traversal / value-formatting rules as ResolveTaskField (missing path → "", scalar → unquoted/stringified, object/array → compact JSON, traversing into a scalar → error). Unlike ResolveTaskField it does no task-specific implicit `payload.` prefixing or `lifecycle` injection — every other Phase 5b PR1 task-context RPC (`boid task current` / `instructions` / `env` / `payload`, docs/plans/phase5-shim-and-task-context.md) shares this generic core so `--field` behaves identically everywhere task_get already established the contract.
func ResolveTaskField ¶
func ResolveTaskField(task *orchestrator.Task, actions orchestrator.LifecycleStore, path string) (string, error)
ResolveTaskField returns the value at the given dotted path against the task.
Path resolution:
- Each dot-separated segment is a JSON key.
- The first segment is matched against top-level Task fields (via JSON tags). When it doesn't match, the path is implicitly resolved inside `payload`, so traits like `awaiting.question`, `artifact.report`, and `lifecycle.abort.message` work without an explicit `payload.` prefix.
- `lifecycle` is a computed trait derived from action history. When lifecycle is referenced, ResolveTaskField calls DeriveLifecycle (when actions is non-nil) and injects the result under `payload.lifecycle` before traversal.
Output format:
- Strings: returned unquoted.
- Numbers and booleans: stringified.
- Objects and arrays: compact JSON.
- Missing path: returns "" with no error.
- Traversing into a scalar with remaining path segments: error.
func SanitizeAttachmentName ¶ added in v0.0.7
SanitizeAttachmentName validates a user-supplied filename and returns the cleaned basename. It rejects anything that doesn't match attachmentNameRe, exceeds the byte cap, has a disallowed extension, or contains only a dot prefix (".env" etc., which the regex would otherwise allow).
func SaveMultipartAttachments ¶ added in v0.0.7
func SaveMultipartAttachments(dataHome, taskID string, files []*multipart.FileHeader) ([]string, error)
SaveMultipartAttachments persists every file under the multipart form field "attachments" into the task-scoped attachments directory. It enforces:
- per-file size cap (AttachmentMaxFileBytes)
- aggregate task-dir size cap (AttachmentMaxTotalBytes)
- filename sanitization (SanitizeAttachmentName)
On success it returns the list of saved basenames. On any error it removes the files it had partially written during this call so the caller can surface the error without leaving orphans behind.
func ValidateAttachmentHeaders ¶ added in v0.0.7
func ValidateAttachmentHeaders(files []*multipart.FileHeader) error
ValidateAttachmentHeaders runs the size + name checks that SaveMultipartAttachments would apply, without touching the filesystem. Web handlers call this *before* creating the task so a bad upload (oversized, bad extension, disallowed name) doesn't leave a half-created task behind.
Types ¶
type ActionApplication ¶
type ActionApplication struct {
Task *orchestrator.Task `json:"task"`
Action *orchestrator.Action `json:"action"`
MatchedHooks []string `json:"matched_hooks,omitempty"`
}
type ActionHandler ¶
type ActionHandler struct {
Service WorkflowService
}
func (*ActionHandler) Apply ¶
func (h *ActionHandler) Apply(w http.ResponseWriter, r *http.Request)
func (*ActionHandler) Routes ¶
func (h *ActionHandler) Routes() chi.Router
type ActionStore ¶
type ActionStore interface {
CreateAction(action *orchestrator.Action) error
ListActionsByTask(taskID string) ([]*orchestrator.Action, error)
}
type AnswerTaskRequest ¶
type ApplyActionRequest ¶
type ApplyActionRequest struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload,omitempty"`
}
type BlockingAskRegistry ¶ added in v0.0.6
type BlockingAskRegistry struct {
// contains filtered or unexported fields
}
BlockingAskRegistry coordinates harness-independent blocking Q&A. When an agent calls `boid task ask`, the broker holds the RPC connection open and the server-side handler blocks in Wait until the user/supervisor answers (via TaskHandler.Answer → AnswerTask → Notify) or the context is cancelled (daemon shutdown / agent disconnect).
The registry is purely in-memory: a daemon restart drops every pending ask. That is consistent — the sandbox process backing the blocked RPC dies with the daemon too, so there is nothing to resume.
func NewBlockingAskRegistry ¶ added in v0.0.6
func NewBlockingAskRegistry() *BlockingAskRegistry
NewBlockingAskRegistry returns an initialised registry.
func (*BlockingAskRegistry) Cancel ¶ added in v0.0.6
func (r *BlockingAskRegistry) Cancel(qid string)
Cancel removes the registration for qid (and the task it belongs to). It does not unblock Wait by itself — a blocked Wait observes ctx cancellation — but it frees the B1 slot so the task can ask again. Idempotent and safe to defer.
func (*BlockingAskRegistry) Has ¶ added in v0.0.6
func (r *BlockingAskRegistry) Has(qid string) bool
Has reports whether qid currently has a registration. Intended for tests and diagnostics.
func (*BlockingAskRegistry) Notify ¶ added in v0.0.6
func (r *BlockingAskRegistry) Notify(qid, answer string) bool
Notify delivers answer to the waiter registered for qid. It returns true when a registration exists and the answer was accepted, false when no waiter is registered (e.g. the agent already disconnected and Cancel ran) or an answer was already delivered. The send is non-blocking thanks to the buffered channel, so Notify never blocks even if Wait has not yet been scheduled.
func (*BlockingAskRegistry) Register ¶ added in v0.0.6
func (r *BlockingAskRegistry) Register(taskID, qid string) error
Register reserves the answer channel for (taskID, qid). It MUST be called before the task transitions to awaiting so an answer that arrives immediately afterwards is never dropped. The check-and-insert is atomic under the registry lock, so two concurrent asks for the same task can never both succeed.
B1 (relaxed): a second pending ask for the same task fails with ErrAskPending ONLY when it carries a DIFFERENT question id. Re-registering the SAME qid is a re-attach — an agent whose `boid task ask` was killed by a harness command-timeout retrying the identical question — and is allowed. The re-attach installs a fresh channel; any prior waiter is already tearing down on its cancelled context. (Even if its deferred Cancel later clobbers this channel, the durable PendingAnswer path backstops delivery, so at worst the fast path degrades to one extra ask round-trip.)
func (*BlockingAskRegistry) Wait ¶ added in v0.0.6
Wait blocks until an answer is delivered for qid via Notify or ctx is cancelled. qid must have been registered; an unknown qid is a programming error and returns immediately. The caller is responsible for Cancel(qid) cleanup (typically via defer) on every exit path.
type BrokerHandler ¶
type BrokerHandler struct {
Registry BrokerRegistry
}
func (*BrokerHandler) Register ¶
func (h *BrokerHandler) Register(w http.ResponseWriter, r *http.Request)
func (*BrokerHandler) Routes ¶
func (h *BrokerHandler) Routes() chi.Router
type BrokerRegisterRequest ¶
type BrokerRegisterRequest struct {
Commands map[string]orchestrator.HostCommandSpec `json:"commands"`
BuiltinPolicies map[string]sandbox.BuiltinPolicy `json:"builtin_policies,omitempty"`
ProjectID string `json:"project_id,omitempty"`
}
type BrokerRegisterResponse ¶
type BrokerRegisterResponse struct {
Token string `json:"token"`
Socket string `json:"socket"`
// ResolvedHostCommands used to echo back
// dispatcher.ResolveHostCommands' absolute-path-keyed map so `boid exec`
// could feed it into SandboxRuntimeInfo (matching shim bind-mount
// targets to broker policy keys without re-resolving on the client
// side). That map has been dead weight since the Phase 5 5a-3 cutover
// retired the shim's absolute-host-path bind mount scheme
// (SandboxRuntimeInfo.ResolvedHostCommands was deleted, hostCommandMounts
// and buildHostCommandNamesEnv with it — see
// docs/plans/phase5-shim-and-task-context.md 5a PR3). No client reads
// this field any more; it is retained as omitempty for one release so
// any stale caller decoding the response doesn't blow up on a missing
// key, and will be removed outright once no versions of the CLI still
// speak the older shape.
ResolvedHostCommands map[string]orchestrator.CommandDef `json:"resolved_host_commands,omitempty"`
}
type BrokerRegistry ¶
type BrokerRegistry interface {
RegisterBrokerCommands(commands map[string]orchestrator.HostCommandSpec, builtinPolicies map[string]sandbox.BuiltinPolicy, projectID string) (*BrokerRegisterResponse, error)
}
type ConfigHandler ¶ added in v0.0.13
type ConfigHandler struct {
Service ConfigService
}
func (*ConfigHandler) KitsDir ¶ added in v0.0.13
func (h *ConfigHandler) KitsDir(w http.ResponseWriter, r *http.Request)
KitsDir handles GET /api/config/kits-dir: the daemon's effective KitsDir, so a CLI client-side helper (cmd/workspace.go's ensureWorkspaceExistsForAssign, MAJOR 1) can resolve a legacy workspace.yaml's `kits:` references against the *running daemon's* kits directory instead of independently re-deriving a default that silently disagrees with it whenever the daemon was started with a custom --kits-dir.
func (*ConfigHandler) Routes ¶ added in v0.0.13
func (h *ConfigHandler) Routes() chi.Router
type ConfigService ¶ added in v0.0.13
type ConfigService interface {
// KitsDir returns the daemon's effective base directory for installed
// kits (server.Config.KitsDir, resolved once at startup — see
// cmd/start.go's defaultKitsDir()/--kits-dir flag).
KitsDir() string
}
ConfigService is the daemon-side dependency behind GET /api/config/kits-dir (MAJOR 1, codex review round 1, docs/plans/workspace-db-consolidation.md Phase 2.5 PR7): a small read-only surface exposing daemon startup config that only the running daemon actually knows, since it may have been overridden at `boid start` time (--kits-dir) and never reaches whatever default a CLI client-side helper would otherwise (re-)derive on its own. Implemented directly by *server.Server (its KitsDir() method already matches this shape) — no adapter type is needed, mirroring HostCommandsService's own convention in this package.
type CreateProjectRequest ¶
type CreateProjectRequest struct {
WorkDir string `json:"work_dir"`
}
type CreateTaskRequest ¶
type CreateTaskRequest struct {
ID string `json:"id,omitempty"`
ProjectID string `json:"project_id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Behavior string `json:"behavior,omitempty"`
BehaviorSpec *orchestrator.BehaviorSpec `json:"behavior_spec,omitempty"`
RemoteID string `json:"remote_id,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
Instructions json.RawMessage `json:"instructions,omitempty"`
AutoStart bool `json:"auto_start,omitempty"`
Traits []string `json:"traits,omitempty"`
Ref string `json:"ref,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Readonly *bool `json:"readonly,omitempty"`
}
type DeviceAuthHandler ¶ added in v0.0.13
type DeviceAuthHandler struct {
Pairing loginPairing
Store deviceAuthStore
Limiter loginRateLimiter
Registry *auth.ConnectionRegistry
PublicURL string
}
DeviceAuthHandler serves the Bearer device-auth endpoints used by remote CLI clients (docs/plans/cli-remote-connection.md Phase 3 PR0):
- POST /api/auth/device — public, rate-limited. Redeems a pairing code (the same one `boid web pair` issues for the cookie flow) for a long-lived Bearer device token.
- DELETE /api/auth/devices/{id} — Bearer-authenticated, self-revoke only.
It is deliberately a separate surface from WebManagementHandler (mounted at /api/web/*, UNIX-socket-only, trusted local-admin surface with no ownership check: /api/web/pair, /api/web/devices) — this handler is reachable over TCP by a caller who starts out holding nothing but a valid pairing code, so PostDevice carries its own rate limiting and DeleteDevice enforces "only revoke yourself".
func (*DeviceAuthHandler) DeleteDevice ¶ added in v0.0.13
func (h *DeviceAuthHandler) DeleteDevice(w http.ResponseWriter, r *http.Request)
DeleteDevice revokes the caller's own device only, and only when the caller authenticated via Bearer token. The {id} path param must match the Bearer-authenticated device ID the auth middleware placed in the request context (auth.DeviceIDFromContext + auth.AuthMethodFromContext).
The auth-method check exists because a session-cookie caller has other, richer paths for managing their own devices (the WebManagementHandler /api/web/devices UNIX-socket surface used by `boid web revoke`) — this endpoint is explicitly the *Bearer* self-revoke path used by `boid logout`, and mixing the two auth models on the same route would muddle the responsibility split.
This is intentionally stricter than WebManagementHandler.DeleteDevice (UNIX-socket-only local admin, no ownership check) — this endpoint is reachable by any Bearer-holding remote caller, so it must not let one device revoke another's token.
func (*DeviceAuthHandler) PostDevice ¶ added in v0.0.13
func (h *DeviceAuthHandler) PostDevice(w http.ResponseWriter, r *http.Request)
PostDevice redeems a one-time pairing code (auth.PairingManager — the same manager instance the cookie-based /login and /auth flows share) for a long-lived Bearer device token. Unlike those flows, no session cookie is ever set here: the raw token is returned exactly once, in the response body, and only its SHA-256 hash (auth.HashToken) is ever persisted (Store.InsertDeviceToken).
func (*DeviceAuthHandler) Routes ¶ added in v0.0.13
func (h *DeviceAuthHandler) Routes() chi.Router
type DeviceGCStore ¶
type DispatchCoordinator ¶
type DispatchCoordinator interface {
DispatchAndAdvance(ctx context.Context, task *orchestrator.Task, meta *orchestrator.ProjectMeta, sm *orchestrator.StateMachine) (*orchestrator.DispatchResult, error)
ReplayHook(ctx context.Context, task *orchestrator.Task, meta *orchestrator.ProjectMeta, sm *orchestrator.StateMachine, hookID string) (*orchestrator.ReplayResult, error)
}
type DuplicateTaskRequest ¶
type DuplicateTaskRequest struct {
AutoStart bool `json:"auto_start"`
}
type ExecDispatcher ¶ added in v0.0.10
type ExecDispatcher interface {
StartExec(ctx context.Context, req StartExecRequest) (*StartExecResult, error)
}
ExecDispatcher launches a JobKindExec job (arbitrary argv, shell harness, no HarnessAdapter agent) through Runner.Dispatch() and returns the runtime job id. Implemented by internal/server's sessionDispatcherAdapter.
type GCAppService ¶
type GCAppService struct {
Store GCStore
DeviceStore DeviceGCStore // optional; deletes revoked devices on GC
}
func (*GCAppService) GC ¶
func (s *GCAppService) GC(olderThan time.Duration, dryRun bool) (*orchestrator.GCResult, error)
GC implements orchestrator.GCStore so GCAppService can be passed to GCLoop.
func (*GCAppService) Run ¶
func (s *GCAppService) Run(olderThan time.Duration, dryRun bool) (*orchestrator.GCResult, error)
type GCHandler ¶
type GCHandler struct {
Service GCService
// RuntimesDir, when non-empty, is server/wire.go's runtimesDirFor(cfg) —
// used to list workspace home directories and their sizes in the
// response (docs/plans/home-workspace-volume.md Phase 4 PR5:
// "サイズ可視化のみで開始、自動 prune なし"). Left empty, the response
// omits workspace_homes entirely — no size listing, and (unchanged from
// pre-PR5) no home directory is ever deleted by GC either way.
RuntimesDir string
// Workspaces, when set, is consulted to flag orphaned home directories
// (a homes/<slug> directory with no corresponding workspace row) in the
// workspace_homes listing. Optional: a nil Workspaces just means every
// entry reports orphan=true (ListWorkspaceHomeSizes's degrade-gracefully
// path — see its doc comment).
Workspaces WorkspaceSlugLister
}
type GlobalJobStore ¶
type GlobalJobStore interface {
ListJobsWithContext(filter JobListFilter) ([]JobWithContext, error)
}
GlobalJobStore supports cross-task job listing with context (task title, project name).
type HookService ¶
type HookService interface {
ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
}
HookService provides hook replay and hook listing operations.
type HostCommandsHandler ¶ added in v0.0.13
type HostCommandsHandler struct {
Service HostCommandsService
}
func (*HostCommandsHandler) List ¶ added in v0.0.13
func (h *HostCommandsHandler) List(w http.ResponseWriter, r *http.Request)
List handles GET /api/host_commands: the sorted list of host_commands names known to the daemon (docs/plans/workspace-db-consolidation.md plan doc contract: "参照名一覧を返す契約"), useful for Web UI / CLI (`boid host-commands list`) validation of a workspace's host_commands references. MINOR 1 (codex review): this used to return the full aggregated definition map (path/env/policy per name) instead of just the name list the plan doc specifies — a caller validating references has no business seeing (or needing to parse) the internal command policy.
func (*HostCommandsHandler) Reload ¶ added in v0.0.13
func (h *HostCommandsHandler) Reload(w http.ResponseWriter, r *http.Request)
Reload handles POST /api/host_commands/reload: re-read the aggregated config from disk after a hand edit (the plan doc's documented way to add/adjust a host_command without a kit).
func (*HostCommandsHandler) Routes ¶ added in v0.0.13
func (h *HostCommandsHandler) Routes() chi.Router
type HostCommandsProvider ¶ added in v0.0.13
type HostCommandsProvider interface {
HostCommands() map[string]orchestrator.HostCommandSpec
}
HostCommandsProvider exposes the daemon's live aggregated host_commands snapshot (name -> spec) for reference-name validation on workspace create/update (docs/plans/workspace-db-consolidation.md MAJOR 2, codex review: an unresolvable meta.HostCommands reference must be rejected with 400 at write time rather than silently persisted and later warned-about + skipped at dispatch). Implemented by *server.Server, which already exposes this exact method for HostCommandsService above.
type HostCommandsService ¶ added in v0.0.13
type HostCommandsService interface {
// HostCommands returns a snapshot of the daemon's aggregated
// host_commands config (raw/unexpanded — see
// orchestrator.ExpandHostCommandsForDispatch's doc comment for why).
HostCommands() map[string]orchestrator.HostCommandSpec
// ReloadHostCommands re-reads ~/.config/boid/host_commands.yaml from
// disk and swaps it in, live, without a daemon restart.
ReloadHostCommands() error
}
HostCommandsService is the daemon-side dependency behind GET /api/host_commands and POST /api/host_commands/reload (docs/plans/workspace-db-consolidation.md PR4 Step G). Implemented directly by *server.Server (its HostCommands()/ReloadHostCommands() methods already match this shape) — no adapter type is needed, unlike some of the other *Handler.Service fields in this package.
type ImportError ¶
type ImportResult ¶
type ImportResult struct {
Created int `json:"created"`
Skipped int `json:"skipped"`
Errors []ImportError `json:"errors"`
}
type Job ¶
type Job struct {
ID string `json:"id"`
TaskID string `json:"task_id"`
ProjectID string `json:"project_id"`
HandlerID string `json:"handler_id"`
DisplayName string `json:"display_name,omitempty"`
Role string `json:"role"`
RuntimeID string `json:"runtime_id,omitempty"`
WorkspacePath string `json:"workspace_path,omitempty"`
Interactive bool `json:"interactive"`
TTY bool `json:"tty"`
Status JobStatus `json:"status"`
ExitCode int `json:"exit_code,omitempty"`
Output string `json:"output,omitempty"`
ExecutionState string `json:"execution_state,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TranscriptSize int64 `json:"transcript_size,omitempty"`
TranscriptMtime *time.Time `json:"transcript_mtime,omitempty"`
TranscriptIdleSeconds int64 `json:"transcript_idle_seconds,omitempty"`
}
type JobCompletion ¶
type JobDoneRequest ¶
type JobHandler ¶
type JobHandler struct {
Jobs JobStore
Global GlobalJobStore // optional: enables cross-task listing when task_id is absent
Service WorkflowService
LogReader JobLogReader // optional: enables static GET /{id}/log
SSEHandler http.Handler // optional: enables SSE streaming for GET /{id}/log?follow=true
}
func (*JobHandler) AgentStop ¶
func (h *JobHandler) AgentStop(w http.ResponseWriter, r *http.Request)
AgentStop asks the daemon to deliver SIGUSR1 to the runtime pgrp. claude.Adapter.Run() catches the signal and forwards SIGTERM to the claude child while the surrounding runner-inner-child keeps running and posts `boid job done --output-file payload_patch.json` through the broker — that callback remains the canonical CompleteJob caller, preserving the agent's session id without racing against UnregisterJob. See WorkflowService.StopAgent for the lifecycle rationale.
func (*JobHandler) Done ¶
func (h *JobHandler) Done(w http.ResponseWriter, r *http.Request)
func (*JobHandler) Get ¶
func (h *JobHandler) Get(w http.ResponseWriter, r *http.Request)
func (*JobHandler) List ¶
func (h *JobHandler) List(w http.ResponseWriter, r *http.Request)
func (*JobHandler) Log ¶
func (h *JobHandler) Log(w http.ResponseWriter, r *http.Request)
func (*JobHandler) Patch ¶ added in v0.0.4
func (h *JobHandler) Patch(w http.ResponseWriter, r *http.Request)
func (*JobHandler) Routes ¶
func (h *JobHandler) Routes() chi.Router
type JobLifecycle ¶
type JobLifecycle interface {
CompleteJob(jobID string, result JobCompletion)
UnregisterJob(jobID string)
CleanupTaskWindow(taskID string)
StopJobRuntime(runtimeID string)
// SignalJobRuntime delivers a single Unix signal to the runtime's process
// group. Phase 3-b uses it to graceful-stop the agent (SIGUSR1) without
// tearing down the surrounding sandbox runtime: claude.Adapter.Run() has a
// signal.Notify(SIGUSR1) handler that translates the group signal into a
// SIGTERM toward the claude child, then normalises the resulting exit
// status into Result.StoppedByDaemon=true.
SignalJobRuntime(runtimeID string, sig syscall.Signal)
}
type JobListFilter ¶
type JobListFilter struct {
Status string
Interactive *bool // nil = no filter
TasklessOnly bool // true = only jobs where task_id IS NULL
}
JobListFilter specifies optional filters for global job listing.
type JobLogReader ¶
type JobLogReader interface {
ReadJobLog(runtimeID string) ([]byte, error)
StatJobLog(runtimeID string) (size int64, mtime time.Time, err error)
}
JobLogReader reads the transcript log for a given runtime.
type JobLogSSEHandler ¶
type JobLogSSEHandler struct {
Subscriber dispatcher.RuntimeSubscriber
Registry *auth.ConnectionRegistry
}
JobLogSSEHandler streams live job output as Server-Sent Events on GET /{id}/log?follow=true.
func (*JobLogSSEHandler) ServeHTTP ¶
func (h *JobLogSSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type JobWithContext ¶
type JobWithContext struct {
Job
TaskTitle string `json:"task_title"`
ProjectName string `json:"project_name"`
}
JobWithContext extends Job with task and project metadata for the TUI global view.
type LoginHandler ¶
type LoginHandler struct {
Pairing loginPairing
Signer loginSigner
Store loginDeviceStore
Limiter loginRateLimiter
}
LoginHandler handles /login and /auth.
func (*LoginHandler) GetAuth ¶
func (h *LoginHandler) GetAuth(w http.ResponseWriter, r *http.Request)
func (*LoginHandler) GetLogin ¶
func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request)
func (*LoginHandler) PostLogin ¶
func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request)
type MetaStore ¶
type MetaStore interface {
Get(id string) (*orchestrator.ProjectMeta, bool)
// GetWithWorkspace returns the project meta with workspace.yaml (kits,
// env, capabilities) hydrated in. Use this whenever the caller dispatches
// hooks or otherwise needs the resolved runtime view.
GetWithWorkspace(ctx context.Context, projectID string) (*orchestrator.ProjectMeta, error)
}
type Notifier ¶
Notifier sends an agent-driven notification for a task. Implementations typically exec a user-configured command. nil-safe at the call site: TaskAppService.NotifyTask returns an error when Notify is unset.
type NotifyTaskRequest ¶
type Pairer ¶
WebManagementHandler serves the CLI management API at /api/web/*. All routes are accessible only via UNIX socket (CLI control plane). Pairer issues pairing codes.
type ProjectAppService ¶
type ProjectAppService struct {
Projects ProjectRepository
Meta interface {
Load(workDir string) (*orchestrator.ProjectMeta, error)
Get(id string) (*orchestrator.ProjectMeta, bool)
Remove(id string)
LoadAll(projects []*orchestrator.Project) []error
SetWorkspaceID(projectID, workspaceID string)
}
// Hydrator is optional. When set, callers that need fully-resolved meta
// (workspace-level capabilities / host_commands / env merged in +
// SecretNamespace injected) go through GetWithWorkspace instead of the
// bare Meta.Get path.
// Wired in internal/server/wire.go alongside the workspace store.
Hydrator orchestrator.MetaHydrator
// CaptureUpstreamURL reads a project work_dir's git origin remote and
// normalizes it to HTTPS (docs/plans/git-gateway-cutover.md PR2). Wired
// in internal/server/wire.go to dispatcher.CaptureUpstreamURL. When nil
// (e.g. in tests that do not exercise this path), upstream_url capture
// is skipped entirely rather than panicking.
CaptureUpstreamURL func(workDir string) (string, error)
// Workspaces provides direct CRUD over a single workspace's
// WorkspaceMeta (docs/plans/workspace-db-consolidation.md PR4). Wired in
// internal/server/wire.go to ProjectStore.WorkspaceStore(). Required for
// the workspace create/show/update/remove endpoints; callers that never
// exercise those (most existing tests) can leave it nil.
Workspaces WorkspaceStore
// HostCommands, when set, returns the daemon's live aggregated
// host_commands snapshot (name -> spec). CreateWorkspace/UpdateWorkspace
// validate every meta.HostCommands reference against this snapshot,
// rejecting an unknown name with 400 (docs/plans/
// workspace-db-consolidation.md MAJOR 2, codex review: an unresolvable
// reference was previously persisted silently and only warned-about +
// skipped at dispatch time). Wired in internal/server/wire.go to
// Server.HostCommands. nil skips the validation entirely — tests that do
// not exercise it can leave this unset, same convention as
// CaptureUpstreamURL/Hydrator above.
HostCommands func() map[string]orchestrator.HostCommandSpec
// contains filtered or unexported fields
}
func (*ProjectAppService) CreateProject ¶
func (s *ProjectAppService) CreateProject(workDir string) (*orchestrator.Project, error)
func (*ProjectAppService) CreateWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) CreateWorkspace(slug string, meta *orchestrator.WorkspaceMeta) (*WorkspaceDetail, error)
CreateWorkspace inserts a brand-new workspace at slug (docs/plans/workspace-db-consolidation.md PR4 Step C, POST /api/workspaces). See the ProjectService interface doc comment for the status code contract.
func (*ProjectAppService) DeleteProject ¶
func (s *ProjectAppService) DeleteProject(id string) error
func (*ProjectAppService) ExportWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) ExportWorkspace(slug string) ([]byte, string, error)
ExportWorkspace returns slug's raw yaml body (the marshaled WorkspaceMeta with a top-level "slug:" key inlined) and its current revision (docs/plans/workspace-db-consolidation.md PR5 Step A).
The body shape mirrors CreateWorkspace / ImportWorkspace's input shape exactly — same top-level "slug:" key alongside the meta fields, decoded by the same DecodeWorkspaceCreateStrict — so `boid workspace export` can pipe straight into `boid workspace import` (or the API can round-trip export → POST /api/workspaces/import) without any translation step (codex PR5 review, MAJOR: round-trip 非対称は避ける、 CLI --slug は override 用の便宜として残す)。 An earlier iteration deliberately omitted "slug:" from the body — "the slug is already in the URL" — but that meant the exported yaml was NOT a valid import body, which is exactly the shape a round-trip needs.
Meta and revision are read from a single atomic snapshot (LoadWithRevision), the same discipline GetWorkspace's MAJOR 1 fix established — see that method's doc comment for why a separate meta/ revision query pair would risk straddling a concurrent write.
func (*ProjectAppService) GetProject ¶
func (s *ProjectAppService) GetProject(id string) (*orchestrator.Project, error)
func (*ProjectAppService) GetWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) GetWorkspace(slug string) (*WorkspaceDetail, error)
GetWorkspace returns slug's meta, revision, and assigned project ids (docs/plans/workspace-db-consolidation.md PR4 Step D, GET /api/workspaces/{slug}).
MAJOR 1 (codex review): meta and revision are read from a single atomic snapshot (Workspaces.LoadWithRevision) rather than two separate queries (a plain Load for meta, then GetWorkspaceSummary for revision) — the pre-fix two-query approach could straddle a concurrent PUT and return a meta/revision pair that never coexisted in the DB.
func (*ProjectAppService) ImportWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) ImportWorkspace(slug string, meta *orchestrator.WorkspaceMeta, mode string) (*WorkspaceDetail, error)
ImportWorkspace inserts (mode=create-only) or upserts (mode=replace) slug's workspace meta from a yaml import body (docs/plans/ workspace-db-consolidation.md PR5 Step C, POST /api/workspaces/import). The body shape mirrors CreateWorkspace's (slug inlined in the yaml, decoded by the same DecodeWorkspaceCreateStrict the handler calls before this runs) — mode=create-only reuses exactly the same Workspaces.Create path CreateWorkspace does (409 on an existing slug); mode=replace reuses Workspaces.Save, which is upsert-on-conflict (see orchestrator.WorkspaceRepository.Save's doc comment), so it both creates a missing slug and overwrites an existing one wholesale — a true upsert, unlike UpdateWorkspace's PUT semantics which 404 on a missing slug.
mode is validated here, not only in the handler (the same belt-and-suspenders duplication ValidWorkspaceSlug gets throughout this file): an unrecognized mode is rejected with 400 before anything is materialized or persisted, so a caller invoking ImportWorkspace directly (bypassing WorkspaceHandler) still gets the same guarantee.
func (*ProjectAppService) ListProjects ¶
func (s *ProjectAppService) ListProjects(workspaceID string) ([]*orchestrator.Project, error)
func (*ProjectAppService) ListWorkspaces ¶
func (s *ProjectAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
func (*ProjectAppService) ReloadProjects ¶
func (s *ProjectAppService) ReloadProjects() (*ProjectReloadResult, error)
func (*ProjectAppService) RemoveWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) RemoveWorkspace(slug string) error
RemoveWorkspace deletes slug (docs/plans/workspace-db-consolidation.md PR4 Step F, DELETE /api/workspaces/{slug}). The reserved default workspace cannot be removed (decision 8); any project still assigned to slug is re-pointed at default by orchestrator.WorkspaceRepository.Remove's transaction, so this never leaves a dangling project_workspaces reference at the DB layer.
That DB-level transaction has no way to reach into the daemon's in-memory ProjectStore cache (s.Meta), though — so without the snapshot-then-sync dance below, every project that *was* assigned to slug would keep serving GetWithWorkspace hydration off a stale cached workspace_id that no longer has a corresponding workspaces row, silently degrading (host_commands/env/ capabilities not injected, same as the pre-existing "workspace.yaml not found" degraded-mode path) until the next daemon restart. Snapshotting the assigned project ids *before* the delete (list ordering matters: after the delete, ListWorkspaces/ListProjects would already report them as belonging to default and we'd have nothing left to distinguish "was reassigned, needs a cache sync" from "already was on default") then calling s.Meta.SetWorkspaceID for each afterward mirrors the exact same cache update SetProjectWorkspace already performs for a single explicit assignment.
MAJOR 3 (codex review): the whole snapshot -> DB remove -> cache-sync sequence below runs under s.mu (see that field's doc comment), serialized against SetProjectWorkspace's own critical section. Without this, a concurrent SetProjectWorkspace reassigning one of the snapshotted projects to a *different*, still-existing workspace could have its cache write clobbered by this method's stale-snapshot loop landing after it — leaving the in-memory cache pointing at "default" while the DB (and any fresh GetWithWorkspace hydration) says the project belongs to the new workspace.
func (*ProjectAppService) ResolveProjectRef ¶
func (s *ProjectAppService) ResolveProjectRef(ref string) ([]*orchestrator.Project, error)
ResolveProjectRef resolves ref to matching projects with the following priority:
- id exact match (returns immediately on first hit)
- name exact match (all projects with that name)
- name substring match, case-insensitive
Returns a single-element slice on unambiguous match, a multi-element slice on ambiguous match, or StatusError{404} when nothing matches.
func (*ProjectAppService) SetProjectWorkspace ¶
func (s *ProjectAppService) SetProjectWorkspace(id, workspaceID string) (*orchestrator.Project, error)
func (*ProjectAppService) UpdateWorkspace ¶ added in v0.0.13
func (s *ProjectAppService) UpdateWorkspace(slug string, meta *orchestrator.WorkspaceMeta, ifMatch string, force bool) (*WorkspaceDetail, error)
UpdateWorkspace replaces slug's meta wholesale (docs/plans/ workspace-db-consolidation.md PR4 Step E, PUT /api/workspaces/{slug}), enforcing optimistic concurrency: ifMatch must equal the workspace's current revision unless force is true (decision 17 — PUT + If-Match, no PATCH). A missing ifMatch (and !force) is rejected with 428 Precondition Required rather than silently proceeding, since accepting an unconditional PUT by default would make the ETag check opt-in instead of the intended opt-out (--force).
MAJOR 1 (codex review): the non-force path no longer reads the current revision and then Saves unconditionally as two separate statements — that window let two concurrent PUTs starting from the same If-Match both pass their check before either had written, silently losing one writer's update, and let a DELETE landing between a GET and a subsequent PUT resurrect the workspace via Save's upsert semantics. Instead, Workspaces.UpdateIfRevisionMatches performs the check-and-write as one atomic UPDATE; matched=false is then disambiguated into 404 (slug is truly gone) vs 412 (slug exists, but ifMatch is stale) with a follow-up existence check — that follow-up races nothing of consequence, since it only picks the HTTP status code for an already-failed write, not any data mutation.
force=true intentionally keeps the old "check existence, then unconditional Save" shape: force explicitly opts out of the revision guarantee (last-write-wins), so there is no CAS invariant left to protect — but Save is upsert-on-conflict, so a plain Save would silently *create* slug if it did not already exist, which PUT (unlike POST) must not do; hence the explicit existence check before it.
type ProjectHandler ¶
type ProjectHandler struct {
Service ProjectService
SessionDispatcher SessionDispatcher // optional; nil disables the start-session endpoint
ExecDispatcher ExecDispatcher // optional; nil disables the exec endpoint
}
func (*ProjectHandler) Create ¶
func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) Delete ¶
func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) Get ¶
func (h *ProjectHandler) Get(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) List ¶
func (h *ProjectHandler) List(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) Reload ¶
func (h *ProjectHandler) Reload(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) Routes ¶
func (h *ProjectHandler) Routes() chi.Router
func (*ProjectHandler) SetWorkspace ¶
func (h *ProjectHandler) SetWorkspace(w http.ResponseWriter, r *http.Request)
func (*ProjectHandler) StartExec ¶ added in v0.0.10
func (h *ProjectHandler) StartExec(w http.ResponseWriter, r *http.Request)
StartExec handles POST /api/projects/{id}/exec. The project is resolved from the URL ref the same way every other project route does; the body specifies argv and the optional readonly / interactive / display_name knobs. Unlike StartSession there is no top-level /api/exec — every exec is project-scoped by construction (`boid exec -p <ref> -- argv...`).
func (*ProjectHandler) StartSession ¶ added in v0.0.6
func (h *ProjectHandler) StartSession(w http.ResponseWriter, r *http.Request)
StartSession handles POST /api/projects/{id}/sessions. The project is resolved from the URL ref (so refs like a name or short id work the same way other project routes do); the body specifies harness_type and the optional knobs (instruction / readonly / model / session_id / display_name).
type ProjectReloadResult ¶
type ProjectRepository ¶
type ProjectRepository interface {
CreateProject(project *orchestrator.Project) error
GetProject(id string) (*orchestrator.Project, error)
ListProjects() ([]*orchestrator.Project, error)
SetProjectWorkspace(projectID, workspaceID string) error
ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
DeleteProject(id string) error
// SetProjectUpstreamURL persists a project's captured upstream_url (see
// docs/plans/git-gateway-cutover.md PR2). Used by ReloadProjects'
// recapture and by the daemon-startup backfill.
SetProjectUpstreamURL(projectID, upstreamURL string) error
// WorkspaceExists reports whether workspaceID refers to an existing
// workspaces table row. Used by ProjectAppService.SetProjectWorkspace to
// reject assignment to a nonexistent slug (docs/plans/
// workspace-db-consolidation.md MAJOR 5 codex review fix).
WorkspaceExists(workspaceID string) (bool, error)
// AssignWorkspaceIfExists atomically checks-then-assigns in a single DB
// transaction (docs/plans/workspace-db-consolidation.md MAJOR 3, codex
// review), replacing the WorkspaceExists+SetProjectWorkspace two-step
// ProjectAppService.SetProjectWorkspace used before: a DELETE landing
// between those two separate calls could leave a dangling
// project_workspaces reference. Returns an error wrapping os.ErrNotExist
// when workspaceID has no corresponding row (DefaultWorkspaceSlug and ""
// are exempt from the check — see the implementation's doc comment).
AssignWorkspaceIfExists(projectID, workspaceID string) error
// GetWorkspaceSummary returns a single workspace's project count and
// revision, or an error wrapping os.ErrNotExist. Used by the workspace
// CRUD handlers (docs/plans/workspace-db-consolidation.md PR4) to build
// responses and to read the current revision for the PUT If-Match check.
GetWorkspaceSummary(slug string) (*orchestrator.WorkspaceSummary, error)
}
type ProjectService ¶
type ProjectService interface {
CreateProject(workDir string) (*orchestrator.Project, error)
ListProjects(workspaceID string) ([]*orchestrator.Project, error)
ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
GetProject(id string) (*orchestrator.Project, error)
SetProjectWorkspace(id, workspaceID string) (*orchestrator.Project, error)
DeleteProject(id string) error
ReloadProjects() (*ProjectReloadResult, error)
// ResolveProjectRef resolves a ref string to one or more matching projects.
// Priority: id exact match > name exact match > name substring match (case-insensitive).
// Returns 1 project on unambiguous match, multiple on ambiguous match, StatusError{404} on no match.
ResolveProjectRef(ref string) ([]*orchestrator.Project, error)
// CreateWorkspace inserts a brand-new workspace (docs/plans/
// workspace-db-consolidation.md PR4, POST /api/workspaces). Returns a
// *StatusError{409} when slug already has a row, {400} for an invalid
// slug.
CreateWorkspace(slug string, meta *orchestrator.WorkspaceMeta) (*WorkspaceDetail, error)
// GetWorkspace returns slug's meta, revision, and assigned project ids
// (GET /api/workspaces/{slug}). *StatusError{404} when slug is unknown.
GetWorkspace(slug string) (*WorkspaceDetail, error)
// UpdateWorkspace replaces slug's meta wholesale (PUT
// /api/workspaces/{slug}), enforcing optimistic concurrency via ifMatch
// against the workspace's current revision unless force is true.
// *StatusError{428} missing ifMatch, {412} stale ifMatch, {404} unknown
// slug.
UpdateWorkspace(slug string, meta *orchestrator.WorkspaceMeta, ifMatch string, force bool) (*WorkspaceDetail, error)
// RemoveWorkspace deletes slug (DELETE /api/workspaces/{slug}).
// *StatusError{400} for the reserved default slug, {404} unknown slug.
RemoveWorkspace(slug string) error
// ExportWorkspace returns slug's raw yaml body (the marshaled
// WorkspaceMeta, deliberately with no top-level "slug" key — the slug is
// already known from the URL this backs) and its current revision (GET
// /api/workspaces/{slug}/export, docs/plans/workspace-db-consolidation.md
// PR5 Step A). *StatusError{404} when slug is unknown.
ExportWorkspace(slug string) (yamlBytes []byte, revision string, err error)
// ImportWorkspace inserts (mode="create-only") or upserts
// (mode="replace") slug's workspace meta from an import body (POST
// /api/workspaces/import?mode=..., PR5 Step B/C). Unlike
// CreateWorkspace/UpdateWorkspace, mode="replace" is a true upsert: it
// creates slug if absent and overwrites it wholesale (last-write-wins,
// no If-Match) if present. *StatusError{409} for mode="create-only"
// against an existing slug, {400} for an invalid slug, an unrecognized
// mode value, or an unknown host_commands reference.
ImportWorkspace(slug string, meta *orchestrator.WorkspaceMeta, mode string) (*WorkspaceDetail, error)
}
type ProjectWorkDirLookup ¶
type ProjectWorkDirLookup interface {
GetProject(id string) (*orchestrator.Project, error)
}
ProjectWorkDirLookup provides read access to a project's working directory.
type ReopenTaskRequest ¶
type ReopenTaskRequest struct {
Message string `json:"message,omitempty"`
}
type ReplayHookRequest ¶
type ReplayHookRequest struct {
HookID string
Status string // optional: override task.Status before replay
}
ReplayHookRequest is the input for hook replay.
type ReplayHookResult ¶
type ReplayHookResult struct {
Task *orchestrator.Task `json:"task"`
FiredEvents []orchestrator.FiredEvent `json:"fired_events,omitempty"`
}
ReplayHookResult is the output of a hook replay.
type RerunTaskRequest ¶
type RerunTaskRequest struct {
AutoStart bool `json:"auto_start,omitempty"`
InstructionsOverride json.RawMessage `json:"instructions_override,omitempty"`
}
type SecretHandler ¶
type SecretHandler struct {
Store SecretStore
}
func (*SecretHandler) Delete ¶
func (h *SecretHandler) Delete(w http.ResponseWriter, r *http.Request)
func (*SecretHandler) GetValue ¶
func (h *SecretHandler) GetValue(w http.ResponseWriter, r *http.Request)
func (*SecretHandler) List ¶
func (h *SecretHandler) List(w http.ResponseWriter, r *http.Request)
func (*SecretHandler) Routes ¶
func (h *SecretHandler) Routes() chi.Router
func (*SecretHandler) Set ¶
func (h *SecretHandler) Set(w http.ResponseWriter, r *http.Request)
type SecretStore ¶
type SessionDispatcher ¶ added in v0.0.6
type SessionDispatcher interface {
StartSession(ctx context.Context, req StartSessionRequest) (*StartSessionResult, error)
}
SessionDispatcher launches a session job (claude / codex / opencode under a HarnessAdapter) and returns the runtime job id.
type SessionHandler ¶ added in v0.0.6
type SessionHandler struct {
Service ProjectService
Dispatcher SessionDispatcher
}
SessionHandler exposes the POST /api/sessions surface (and the project-scoped /api/projects/{id}/sessions variant mounted by ProjectHandler). Phase 3-d (PR1) introduced sessions as a first-class JobKind alongside hook and exec so user-initiated agent runs (WebUI [New Session] / `boid agent`) no longer have to piggyback on the project command path.
func (*SessionHandler) Routes ¶ added in v0.0.6
func (h *SessionHandler) Routes() chi.Router
func (*SessionHandler) Start ¶ added in v0.0.6
func (h *SessionHandler) Start(w http.ResponseWriter, r *http.Request)
Start handles POST /api/sessions. The request body must specify project_id. Use the project-scoped variant when the project is implied by the URL.
Session-id resume was removed: every Start launches a fresh agent process, so there is no `/api/sessions/{id}/resume` companion route anymore.
type SetProjectWorkspaceRequest ¶
type SetProjectWorkspaceRequest struct {
WorkspaceID string `json:"workspace_id"`
}
type StartExecRequest ¶ added in v0.0.10
type StartExecRequest struct {
// ProjectID is taken from the URL for the project-scoped route; there is
// no top-level /api/exec (every exec is inherently project-scoped —
// `boid exec -p <ref> -- argv...`), so the handler always fills this in
// from chi.URLParam before it reaches the dispatcher.
ProjectID string `json:"project_id"`
// Argv is the literal program + arguments to run inside the sandbox.
// Required, non-empty.
Argv []string `json:"argv"`
// Readonly, when true, mounts the project workspace read-only. Exec
// defaults to writable, matching the CLI's --readonly flag default.
Readonly bool `json:"readonly,omitempty"`
// Interactive requests a PTY-backed sandbox. The CLI computes this from
// isatty(stdin) && isatty(stdout) (see cmd/exec.go) — both, not stdin
// alone, because a PTY is only correct when the whole terminal round-trip
// is real; `boid exec -- cmd | grep pattern` must NOT get a PTY even
// though its own stdin is a real terminal, or the PTY's line-discipline
// framing would corrupt the piped bytes grep receives. false selects the
// plain-pipe transport (see runtime_local_linux.go's non-interactive
// branch and its StdinForward stdin-piping addition).
Interactive bool `json:"interactive,omitempty"`
// DisplayName is the human-readable label persisted to jobs.display_name.
// Empty falls back to argv[0] (dispatcher.BuildExecJobSpec).
DisplayName string `json:"display_name,omitempty"`
}
StartExecRequest is the body of POST /api/projects/{id}/exec. `boid exec` used to be a client-side-only path (the CLI built its own SandboxRuntimeInfo and syscall.Exec'd straight into the sandbox launcher), which is exactly why it never picked up the git gateway cutover's Dispatch()-only wiring (registerGatewayToken / GatewayURL / GatewayCloneURL) — see docs/plans/git-gateway-cutover.md. This request type is the daemon-side entry point that routes exec through the same Runner.Dispatch() path as every session, so any future dispatch-time wiring lands on both by construction instead of needing a second, easy-to-forget call site.
Unlike sessions (fixed harness_type, agent-driven argv), exec runs an arbitrary user-supplied argv with no HarnessAdapter agent — see dispatcher.BuildExecJobSpec, which forces HarnessType="shell" underneath.
type StartExecResult ¶ added in v0.0.10
StartExecResult is the response shape for POST /api/projects/{id}/exec.
type StartSessionRequest ¶ added in v0.0.6
type StartSessionRequest struct {
// ProjectID names the project whose traits the session inherits. For the
// project-scoped route (`/api/projects/{id}/sessions`) it is taken from
// the URL and the body field is ignored.
ProjectID string `json:"project_id"`
// HarnessType selects the agent adapter. Must be one of "claude",
// "codex", or "opencode". The historical "shell" session variant was
// retired after the git gateway cutover — `boid exec -p <project> -- bash`
// runs the shell adapter through the same Runner.Dispatch() with an
// interactive PTY, so there is no session use case left for it.
// sessionDispatcherAdapter.StartSession rejects any other value at the
// API boundary.
HarnessType string `json:"harness_type"`
// Instruction is the optional bootstrap prompt for the first turn. Empty
// leaves the harness to pick its default (no positional for session mode
// on claude, since /boid-task is meaningless without a task.yaml).
Instruction string `json:"instruction,omitempty"`
// Readonly, when true, mounts the project workspace read-only. Sessions
// default to writable (developer ergonomics > fail-safety).
Readonly bool `json:"readonly,omitempty"`
// Model overrides the harness binary's default model selection.
Model string `json:"model,omitempty"`
// DisplayName is the human-readable session label persisted to
// jobs.display_name. Empty falls back to "<harness> session".
DisplayName string `json:"display_name,omitempty"`
}
StartSessionRequest is the body of POST /api/sessions and POST /api/projects/{id}/sessions. Phase 3-d (PR1) introduced the session concept as a first-class JobKind so the WebUI [New Session] dialog and the `boid agent` CLI share one daemon entry point.
type StartSessionResult ¶ added in v0.0.6
type StartSessionResult struct {
JobID string `json:"job_id"`
AttachURL string `json:"attach_url"`
}
StartSessionResult is the response shape for POST /api/sessions and the project-scoped variant.
type StatusError ¶
func (*StatusError) Error ¶
func (e *StatusError) Error() string
type TaskAnswerService ¶
type TaskAnswerService interface {
AnswerTask(ctx context.Context, taskID, questionID, answer string) error
}
TaskAnswerService records a user reply to a pending Q&A question and transitions the task back to executing.
type TaskAppService ¶
type TaskAppService struct {
Tasks TaskStore
Actions ActionStore
Jobs JobStore
Meta MetaStore
Workflow WorkflowService
Projects ProjectWorkDirLookup
RuntimesDir string
Notify Notifier
// BlockingAsk coordinates harness-independent blocking Q&A (boid task ask).
// Shared with the sandbox boid builtin executor (which calls AskTaskBlocking)
// and the answer path (AnswerTask), so both halves of a blocking ask use the
// same in-memory registry. Nil disables blocking ask (notify --ask still works).
BlockingAsk *BlockingAskRegistry
// AskDisconnectGrace is how long an awaiting task may sit with no live agent
// parked before the daemon reclaims it (a blocking ask whose foreground
// command was killed by a harness command-timeout). Zero falls back to
// defaultAskDisconnectGrace.
AskDisconnectGrace time.Duration
}
func (*TaskAppService) AnswerTask ¶
func (s *TaskAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error
AnswerTask saves the user's reply and transitions the task awaiting → executing.
func (*TaskAppService) AskTaskBlocking ¶ added in v0.0.6
func (s *TaskAppService) AskTaskBlocking(ctx context.Context, taskID, question string) (string, error)
AskTaskBlocking implements the harness-independent blocking Q&A RPC behind `boid task ask <question>`. The agent stays alive and blocks inside this call until the user/supervisor answers, so the round-trip works for every harness uniformly — there is no session-resume path anywhere in boid (the legacy `notify --ask` → `claude --resume` flow was removed).
The call is disconnect-resilient and idempotent across re-asks. Harnesses kill long-running shell commands on a command-timeout (claude-code / opencode at ~120s), which severs the held connection. When that happens the model retries the identical `boid task ask`; this handler recognises the retry and recovers instead of treating it as a fresh question:
- executing → fresh ask: register the answer channel, transition executing → awaiting via ApplyAction("ask"), fire the user notification (root tasks only; child tasks are seen by their supervisor's monitor loop), then block in Wait.
- awaiting → re-ask: if an answer was parked durably while the agent was disconnected (PendingAnswer), consume it immediately and flip back to executing. Otherwise re-attach to the SAME question id and block again — no second "ask" transition, no new notification.
Register happens before the awaiting transition so an answer that races in immediately afterwards is never dropped. Decision B1 (relaxed): a concurrent ask carrying a DIFFERENT question id still fails with ErrAskPending; a re-ask of the same question is a re-attach and is allowed.
func (*TaskAppService) CreateTask ¶
func (s *TaskAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
func (*TaskAppService) DeleteTask ¶
func (s *TaskAppService) DeleteTask(id string, force bool) error
func (*TaskAppService) DuplicateTask ¶
func (s *TaskAppService) DuplicateTask(sourceID string, autoStart bool) (*orchestrator.Task, error)
func (*TaskAppService) GetInstructions ¶ added in v0.0.13
func (s *TaskAppService) GetInstructions(id string) ([]orchestrator.RoutedInstruction, error)
GetInstructions returns the task's "active" routed instruction — the entry orchestrator.CurrentInstructions derives from the last entry in the task's instruction history.
NOT the `boid task instructions` RPC's data source (fixed in codex review on PR #797 before merge — see wiring-seams.md #13 and internal/server/boid_executor.go's BoidOpTaskInstructions case): a task row has no notion of "which job is asking", but orchestrator.Evaluator can fire two agent-kind hooks for different agents from the same task in one round (any agent appearing anywhere in the instruction history matches, not just the active/last entry) — GetInstructions would hand a claude job's RPC call the codex hook's instruction whenever codex happens to be the most recent history entry, and vice versa. Kept as a task-row-level projection for callers that genuinely want "the task's current routing state" independent of any specific job (there are none yet in this codebase; a plausible future one is a Web UI task-detail view). Returns an empty (non-nil) slice rather than nil, even when the task carries no active instruction (not executing, or no instructions history yet).
func (*TaskAppService) GetInstructionsField ¶ added in v0.0.13
func (s *TaskAppService) GetInstructionsField(id, path string) (string, error)
GetInstructionsField resolves a dotted path against the []RoutedInstruction list returned by GetInstructions, mirroring GetTaskField's --field contract.
func (*TaskAppService) GetTask ¶
func (s *TaskAppService) GetTask(id string) (*orchestrator.Task, error)
func (*TaskAppService) GetTaskCurrent ¶ added in v0.0.13
func (s *TaskAppService) GetTaskCurrent(id string) (*orchestrator.TaskSnapshot, error)
GetTaskCurrent returns the task's business-metadata snapshot — the same subset historically materialized at $HOME/.boid/context/task.yaml (orchestrator.SnapshotTask), now also the payload of `boid task current`. Unlike the file (frozen at dispatch time), this re-derives from the task row on every call, so it reflects concurrent `task update` calls.
func (*TaskAppService) GetTaskCurrentField ¶ added in v0.0.13
func (s *TaskAppService) GetTaskCurrentField(id, path string) (string, error)
GetTaskCurrentField resolves a dotted path against the task-current snapshot, mirroring GetTaskField's semantics for `--field` (missing path → "", scalar → unquoted/stringified, object/array → compact JSON).
func (*TaskAppService) GetTaskDetail ¶
func (s *TaskAppService) GetTaskDetail(id string) (*TaskDetailView, error)
func (*TaskAppService) GetTaskField ¶
func (s *TaskAppService) GetTaskField(id, path string) (string, error)
GetTaskField resolves a dotted field path against the task. See ResolveTaskField for the path syntax (top-level fields, payload traits, computed lifecycle).
func (*TaskAppService) ImportTasks ¶
func (s *TaskAppService) ImportTasks(reqs []CreateTaskRequest) (*ImportResult, error)
func (*TaskAppService) ListTasks ¶
func (s *TaskAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
func (*TaskAppService) NotifyTask ¶
func (s *TaskAppService) NotifyTask(ctx context.Context, taskID, message, ask, questionID, progress, done, fail string) error
NotifyTask invokes the configured notify command for the given task. Returns 501 when no notifier is wired and ask is empty (notifications disabled in config). When ask is non-empty the task is transitioned to awaiting; the notification is best-effort and skipped if no notifier is configured. questionID identifies the Q&A turn (generated when empty). When progress is non-empty (progress mode), no hook fires and no state transition occurs — only a progress Action is written to the timeline. ask and progress are mutually exclusive.
Note: ask transitions the task to awaiting but does NOT spawn a resume dispatch when the user replies. The session-id resume path was removed (every dispatch is a fresh agent process); only `boid task ask` (the blocking RPC) can deliver an answer back to a live agent.
func (*TaskAppService) RerunTask ¶
func (s *TaskAppService) RerunTask(id string, req RerunTaskRequest) (*orchestrator.Task, error)
func (*TaskAppService) UpdateTask ¶
func (s *TaskAppService) UpdateTask(id string, req UpdateTaskRequest) (*orchestrator.Task, error)
func (*TaskAppService) UpdateTaskPayloadPatch ¶ added in v0.0.13
func (s *TaskAppService) UpdateTaskPayloadPatch(jobID string, patch json.RawMessage, allowedTraits []orchestrator.TraitType) (*orchestrator.Task, error)
UpdateTaskPayloadPatch applies patch to the task owning jobID using the SAME merge semantics the file-based payload_patch.json → job_done → Coordinator pipeline has always applied (orchestrator.MergePayloadPatch, gated by the trait allowlist the firing hook itself declares via Traits.Produces) — see orchestrator/coordinator.go's HandlerResult.allowedTraits and wiring-seams.md #13/#17. This is deliberately NOT UpdateTask's simpler top-level shallow merge (used by --payload-file): UpdateTask has no notion of a "firing hook", so it can't reproduce this gate.
jobID (not taskID) is the identity this method resolves from, because the allowedTraits gate is keyed off the CALLING job's own HandlerID — the specific hook that was dispatched to produce this job, which may differ from other jobs the same task has had or will have (mirrors why BoidOpTaskInstructions/Env/Payload are JobID-scoped, not TaskID-scoped).
allowedTraits is supplied by the CALLER — it must be the value captured AT DISPATCH TIME (dispatcher.JobContextSnapshot.PayloadPatchAllowedTraits, itself sourced from orchestrator.JobSpec.HookTraitsProduces), never re-derived here from a live project-meta lookup. An earlier version of this method did its own live lookup (GetTask's ProjectID -> current meta -> current behavior -> hook by HandlerID) and codex review caught the TOCTOU staleness bug that creates: if project.yaml is edited/reloaded between dispatch and this call, a live lookup can either apply the WRONG (post-edit) trait list or silently fall back to unrestricted when the hook can no longer be found by name — neither matches what was actually authorized when the job was dispatched. Accepting the dispatch-time value as a parameter makes that class of staleness structurally impossible instead of requiring a "fail closed on lookup failure" special case (see wiring-seams.md #17's Major 1 finding). nil means unrestricted — see JobSpec.HookTraitsProduces's own doc comment for exactly when that's the correct (not just the fallback) value.
The GetTask -> MergePayloadPatch -> UpdateTask sequence below is serialized per task id (payloadPatchLockFor) so two concurrent calls for the same task — e.g. two hooks in the same readonly task's parallel dispatch round, each patching a different trait — cannot race a read-modify-write and silently lose one of their writes (Phase 5b PR7 codex review Blocker 2, wiring-seams.md #17).
type TaskDetailView ¶
type TaskDetailView struct {
Task *orchestrator.Task
Actions []*orchestrator.Action
Jobs []*Job
AvailableActions []string `json:"available_actions"`
}
type TaskEventHub ¶
type TaskEventHub struct {
// contains filtered or unexported fields
}
TaskEventHub は taskID ごとに複数の subscriber へイベントを配送する pub-sub ハブ。
func (*TaskEventHub) Broadcast ¶
func (h *TaskEventHub) Broadcast(taskID string, ev TaskEvent)
Broadcast は taskID の全 subscriber に ev を非ブロッキングで配送する。 チャネルが満杯の subscriber はドロップする(hub 全体を止めない)。
type TaskHandler ¶
type TaskHandler struct {
Service TaskService
Hooks HookService // optional: enables hook replay/list when set
Notifier TaskNotifyService // optional: enables POST /{id}/notify when set
Answerer TaskAnswerService // optional: enables POST /{id}/answer when set
}
func (*TaskHandler) Answer ¶
func (h *TaskHandler) Answer(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Create ¶
func (h *TaskHandler) Create(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Delete ¶
func (h *TaskHandler) Delete(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Detail ¶
func (h *TaskHandler) Detail(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Duplicate ¶
func (h *TaskHandler) Duplicate(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Field ¶
func (h *TaskHandler) Field(w http.ResponseWriter, r *http.Request)
Field returns a single dotted-path value extracted from the task. The response is text/plain so it can be consumed directly by shell scripts (e.g. `STATUS=$(boid task show $ID --field status)`).
func (*TaskHandler) Get ¶
func (h *TaskHandler) Get(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Import ¶
func (h *TaskHandler) Import(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) List ¶
func (h *TaskHandler) List(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) ListHooks ¶
func (h *TaskHandler) ListHooks(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Notify ¶
func (h *TaskHandler) Notify(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Patch ¶
func (h *TaskHandler) Patch(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) ReplayHook ¶
func (h *TaskHandler) ReplayHook(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Rerun ¶
func (h *TaskHandler) Rerun(w http.ResponseWriter, r *http.Request)
func (*TaskHandler) Routes ¶
func (h *TaskHandler) Routes() chi.Router
type TaskNotifyService ¶
type TaskNotifyService interface {
NotifyTask(ctx context.Context, taskID, message, ask, questionID, progress, done, fail string) error
}
TaskNotifyService dispatches an agent-driven notification for a task. Wired to *TaskAppService at runtime; left optional on TaskHandler so existing tests do not need to satisfy this interface. ask/questionID are optional Q&A fields: when ask is non-empty the task is transitioned to awaiting after the notification is sent. The session-id resume path was removed, so an answer can only reach an agent that is still alive inside a blocking `boid task ask` RPC. progress is a no-state-change progress note (timeline action only). done / fail are agent self-completion / self-failure: they transition the task to done / aborted respectively and SIGTERM running hook runtimes — the parent supervisor's polling drives the response (verify + optional reopen). ask/progress/done/fail are mutually exclusive.
type TaskService ¶
type TaskService interface {
CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
GetTask(id string) (*orchestrator.Task, error)
GetTaskDetail(id string) (*TaskDetailView, error)
GetTaskField(id, path string) (string, error)
UpdateTask(id string, req UpdateTaskRequest) (*orchestrator.Task, error)
DeleteTask(id string, force bool) error
ImportTasks(reqs []CreateTaskRequest) (*ImportResult, error)
DuplicateTask(sourceID string, autoStart bool) (*orchestrator.Task, error)
RerunTask(id string, req RerunTaskRequest) (*orchestrator.Task, error)
}
type TaskStore ¶
type TaskStore interface {
CreateTask(task *orchestrator.Task) error
GetTask(id string) (*orchestrator.Task, error)
ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
UpdateTask(task *orchestrator.Task) error
DeleteTask(id string) error
FindTaskByRemote(remoteID string) (*orchestrator.Task, error)
FindTaskByRef(ref, parentID string) (*orchestrator.Task, error)
// ListChildren returns direct children (one level only) of the given parent
// task, ordered by created_at ASC. Returns an empty slice (not nil) when the
// task has no children. Used by finalizeTerminal to sweep boid/<id8> branches
// once a supervisor reaches a terminal state.
ListChildren(parentID string) ([]*orchestrator.Task, error)
}
type TaskWorkflowService ¶
type TaskWorkflowService struct {
Tasks TaskStore
Jobs JobStore
Projects ProjectRepository
Tx Transactor
Meta MetaStore
Coordinator DispatchCoordinator
Lifecycle JobLifecycle
Hub *TaskEventHub
// Adapter is the harness adapter used to query post-run usage. Phase 3-b
// dropped the StopAgent role: graceful stop is delivered as a SIGUSR1
// directly via Lifecycle.SignalJobRuntime, which claude.Adapter.Run()'s
// signal.Notify handler intercepts. Adapter remains optional; when nil,
// usage / future per-harness queries become no-ops.
Adapter adapters.HarnessAdapter
// contains filtered or unexported fields
}
func (*TaskWorkflowService) ApplyAction ¶
func (s *TaskWorkflowService) ApplyAction(ctx context.Context, taskID string, req ApplyActionRequest) (*ActionApplication, error)
func (*TaskWorkflowService) CompleteJob ¶
func (s *TaskWorkflowService) CompleteJob(_ context.Context, jobID string, req JobDoneRequest) (*Job, error)
func (*TaskWorkflowService) InitDispatch ¶
func (s *TaskWorkflowService) InitDispatch(ctx context.Context)
InitDispatch initialises the lifecycle context used by dispatch-loop goroutines. Must be called before the first action is applied. The returned cancel is stored internally; call Shutdown to invoke it.
func (*TaskWorkflowService) ListHooksForStatus ¶
func (s *TaskWorkflowService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
ListHooksForStatus returns hooks that match the given status for the task. If status is empty, the task's current status is used.
func (*TaskWorkflowService) ReplayHook ¶
func (s *TaskWorkflowService) ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
ReplayHook replays a single hook for the given task. If req.Status is non-empty the task's status is overwritten before dispatch. Running jobs on the same task cause a 409 Conflict.
func (*TaskWorkflowService) Shutdown ¶
func (s *TaskWorkflowService) Shutdown()
Shutdown cancels the dispatch context and blocks until all in-flight dispatch loops have returned. Call this before closing the database.
func (*TaskWorkflowService) StopAgent ¶
func (s *TaskWorkflowService) StopAgent(runtimeID string)
StopAgent gracefully stops the agent backing runtimeID by delivering SIGUSR1 to its process group. claude.Adapter.Run()'s signal.Notify(SIGUSR1) handler forwards a SIGTERM to the claude child and returns Result.StoppedByDaemon=true, so the surrounding sandbox runtime survives long enough to post `boid job done` through the broker normally. No-op when runtimeID is empty or no JobLifecycle has been configured.
type Transactor ¶
type TxStore ¶
type TxStore interface {
TaskStore
ActionStore
JobStore
}
type UpdateJobRequest ¶ added in v0.0.4
type UpdateJobRequest struct {
DisplayName *string `json:"display_name"`
}
type UpdateTaskRequest ¶
type UpdateTaskRequest struct {
Title string `json:"title"`
Description string `json:"description"`
ProjectID string `json:"project_id,omitempty"`
RemoteID *string `json:"remote_id,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
Instructions json.RawMessage `json:"instructions,omitempty"`
ParentID *string `json:"parent_id,omitempty"`
AutoStart *bool `json:"auto_start,omitempty"`
}
type WSAttachHandler ¶
type WSAttachHandler struct {
Subscriber dispatcher.RuntimeSubscriber
Writer dispatcher.RuntimeInputWriter
PublicURL string
Registry *auth.ConnectionRegistry
// Bearer verifies an `Authorization: Bearer <token>` header carried on
// the WS handshake request (docs/plans/cli-remote-connection.md Phase 3
// PR0). When present, it is checked before auth.DeviceIDFromContext —
// see authenticateDevice's doc comment for the precedence rule. PR3
// moved this handler's mount point in internal/server/wire.go out of
// the cookie-only WebAuthMiddleware Group so a Bearer-only caller (the
// CLI's WS-based AttachJob, internal/client/client.go) can actually
// reach this route end-to-end over TCP; the field itself has existed
// since PR0.
Bearer *auth.BearerVerifier
}
WSAttachHandler handles WebSocket connections for interactive PTY attach. Route: GET /api/jobs/{id}/attach/ws
func (*WSAttachHandler) ServeHTTP ¶
func (h *WSAttachHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type WebAppService ¶
type WebAppService struct {
Tasks TaskStore
Actions ActionStore
Jobs JobStore
GlobalJobs GlobalJobStore
Projects ProjectRepository
Meta MetaStore
Workflow WorkflowService
TaskSvc TaskService
Hooks HookService
Answerer TaskAnswerService // optional: enables POST /tasks/{id}/answer
}
func (*WebAppService) AnswerTask ¶
func (s *WebAppService) AnswerTask(ctx context.Context, taskID, questionID, answer string) error
func (*WebAppService) ApplyAction ¶
func (s *WebAppService) ApplyAction(taskID string, actionType string) error
func (*WebAppService) CreateTask ¶
func (s *WebAppService) CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
func (*WebAppService) DeleteTask ¶
func (s *WebAppService) DeleteTask(id string, force bool) error
DeleteTask delegates to the shared TaskService so the web UI uses the same delete semantics as the JSON API and TUI.
func (*WebAppService) DuplicateTask ¶
func (s *WebAppService) DuplicateTask(id string) (string, error)
DuplicateTask delegates to the shared TaskService so the Web UI uses the same duplication semantics as the JSON API: a fresh task is created via CreateTask + resolveBehavior so that Instructions and Payload come from the behavior's DefaultInstruction / DefaultPayload, not from the source task's runtime state. Without this delegation the duplicate inherited the source's runtime payload (awaiting trait and harness session state) and missing Instructions caused the hook evaluator to skip the agent hook, so no hook fired on Start.
The Web UI button does not auto-start the duplicate; the user clicks Start separately.
func (*WebAppService) GetJob ¶
func (s *WebAppService) GetJob(id string) (*JobWithContext, error)
func (*WebAppService) GetProjectByID ¶
func (s *WebAppService) GetProjectByID(id string) (*orchestrator.Project, error)
func (*WebAppService) GetTaskDetail ¶
func (s *WebAppService) GetTaskDetail(id string) (*TaskDetailView, error)
func (*WebAppService) ListBehaviors ¶
func (s *WebAppService) ListBehaviors() ([]string, error)
func (*WebAppService) ListHooksForStatus ¶
func (s *WebAppService) ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
func (*WebAppService) ListJobs ¶
func (s *WebAppService) ListJobs(status string) ([]JobWithContext, error)
func (*WebAppService) ListProjects ¶
func (s *WebAppService) ListProjects() ([]*orchestrator.Project, error)
func (*WebAppService) ListSessions ¶ added in v0.0.2
func (s *WebAppService) ListSessions() ([]JobWithContext, error)
func (*WebAppService) ListTasks ¶
func (s *WebAppService) ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
func (*WebAppService) ListWorkspaces ¶
func (s *WebAppService) ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
func (*WebAppService) ReopenTask ¶
func (s *WebAppService) ReopenTask(id string, req ReopenTaskRequest) error
func (*WebAppService) ReplayHook ¶
func (s *WebAppService) ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
func (*WebAppService) RerunTask ¶
func (s *WebAppService) RerunTask(id string, req RerunTaskRequest) error
func (*WebAppService) UpdateTask ¶
func (s *WebAppService) UpdateTask(id string, req UpdateTaskRequest) error
type WebHandler ¶
type WebHandler struct {
Service WebService
Hub *TaskEventHub
SessionDispatcher SessionDispatcher
Registry *auth.ConnectionRegistry
// AttachmentsRoot is the data-home directory under which per-task
// attachments (`tasks/<id>/attachments`) are persisted. When empty (e.g.
// :memory: DB during tests) the multipart code path falls back to
// rejecting attachments while still accepting plain form-urlencoded
// submissions.
AttachmentsRoot string
}
func (*WebHandler) GetTaskEdit ¶
func (h *WebHandler) GetTaskEdit(w http.ResponseWriter, r *http.Request)
func (*WebHandler) HookReplayList ¶
func (h *WebHandler) HookReplayList(w http.ResponseWriter, r *http.Request)
func (*WebHandler) JobDetail ¶
func (h *WebHandler) JobDetail(w http.ResponseWriter, r *http.Request)
func (*WebHandler) JobTerminal ¶
func (h *WebHandler) JobTerminal(w http.ResponseWriter, r *http.Request)
JobTerminal redirects legacy deep-links (/jobs/{id}/terminal) to the job detail page (/jobs/{id}), which now renders the terminal inline.
func (*WebHandler) PostAction ¶
func (h *WebHandler) PostAction(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostAnswer ¶
func (h *WebHandler) PostAnswer(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostDelete ¶
func (h *WebHandler) PostDelete(w http.ResponseWriter, r *http.Request)
PostDelete deletes the task and redirects to the task list. Errors are surfaced via ?error= on the same task page so the user sees the reason (e.g. dependents exist).
func (*WebHandler) PostDuplicate ¶
func (h *WebHandler) PostDuplicate(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostEdit ¶
func (h *WebHandler) PostEdit(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostHookReplay ¶
func (h *WebHandler) PostHookReplay(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostReopen ¶
func (h *WebHandler) PostReopen(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostRerun ¶
func (h *WebHandler) PostRerun(w http.ResponseWriter, r *http.Request)
func (*WebHandler) PostStartSession ¶ added in v0.0.6
func (h *WebHandler) PostStartSession(w http.ResponseWriter, r *http.Request)
PostStartSession launches a HarnessAdapter-backed session for the project from the Web UI's [New Session] dialog. Phase 3-d (PR1) introduced this alongside PostProjectExecuteCommand so users can start a claude / codex / opencode session without going through the legacy commands path.
func (*WebHandler) PostTaskCreate ¶
func (h *WebHandler) PostTaskCreate(w http.ResponseWriter, r *http.Request)
func (*WebHandler) QuestionPage ¶
func (h *WebHandler) QuestionPage(w http.ResponseWriter, r *http.Request)
QuestionPage renders the dedicated Q&A turn page at `/tasks/{id}/questions/{question_id}`. The notification deep-link from `boid task notify --ask` lands here. The page shows the question and either an answer form (when this is the active awaiting turn) or the recorded answer (when an answer action exists for the same question_id).
func (*WebHandler) ReopenForm ¶
func (h *WebHandler) ReopenForm(w http.ResponseWriter, r *http.Request)
func (*WebHandler) Routes ¶
func (h *WebHandler) Routes() chi.Router
func (*WebHandler) SessionList ¶ added in v0.0.2
func (h *WebHandler) SessionList(w http.ResponseWriter, r *http.Request)
func (*WebHandler) SessionNew ¶ added in v0.0.2
func (h *WebHandler) SessionNew(w http.ResponseWriter, r *http.Request)
func (*WebHandler) TaskDetail ¶
func (h *WebHandler) TaskDetail(w http.ResponseWriter, r *http.Request)
func (*WebHandler) TaskDetailFragment ¶
func (h *WebHandler) TaskDetailFragment(w http.ResponseWriter, r *http.Request)
TaskDetailFragment returns a partial HTML fragment for the task detail page. The `kind` query parameter selects which section to render:
- "timeline": action history section
- "status": status card + available actions
- "jobs": jobs section
func (*WebHandler) TaskEvents ¶
func (h *WebHandler) TaskEvents(w http.ResponseWriter, r *http.Request)
TaskEvents streams Server-Sent Events for a specific task. Subscribes to the TaskEventHub and forwards events until the client disconnects or the device is revoked.
func (*WebHandler) TaskList ¶
func (h *WebHandler) TaskList(w http.ResponseWriter, r *http.Request)
func (*WebHandler) TaskNew ¶
func (h *WebHandler) TaskNew(w http.ResponseWriter, r *http.Request)
type WebManagementHandler ¶
type WebManagementHandler struct {
Pairing Pairer
Store *auth.Store
PublicURL string
Registry *auth.ConnectionRegistry
}
func (*WebManagementHandler) DeleteAllDevices ¶
func (h *WebManagementHandler) DeleteAllDevices(w http.ResponseWriter, r *http.Request)
func (*WebManagementHandler) DeleteDevice ¶
func (h *WebManagementHandler) DeleteDevice(w http.ResponseWriter, r *http.Request)
func (*WebManagementHandler) GetDevices ¶
func (h *WebManagementHandler) GetDevices(w http.ResponseWriter, r *http.Request)
func (*WebManagementHandler) PostPair ¶
func (h *WebManagementHandler) PostPair(w http.ResponseWriter, r *http.Request)
func (*WebManagementHandler) Routes ¶
func (h *WebManagementHandler) Routes() chi.Router
type WebService ¶
type WebService interface {
ListTasks(filter orchestrator.TaskFilter) ([]*orchestrator.Task, error)
GetTaskDetail(id string) (*TaskDetailView, error)
ListProjects() ([]*orchestrator.Project, error)
ListBehaviors() ([]string, error)
ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
ApplyAction(taskID string, actionType string) error
DuplicateTask(id string) (string, error)
DeleteTask(id string, force bool) error
ListJobs(status string) ([]JobWithContext, error)
ListSessions() ([]JobWithContext, error)
GetJob(id string) (*JobWithContext, error)
CreateTask(req CreateTaskRequest) (*orchestrator.Task, error)
UpdateTask(id string, req UpdateTaskRequest) error
RerunTask(id string, req RerunTaskRequest) error
ReopenTask(id string, req ReopenTaskRequest) error
AnswerTask(ctx context.Context, taskID, questionID, answer string) error
ListHooksForStatus(taskID, status string) ([]orchestrator.Hook, error)
ReplayHook(ctx context.Context, taskID string, req ReplayHookRequest) (*ReplayHookResult, error)
GetProjectByID(id string) (*orchestrator.Project, error)
}
type WorkflowService ¶
type WorkflowService interface {
ApplyAction(ctx context.Context, taskID string, req ApplyActionRequest) (*ActionApplication, error)
CompleteJob(ctx context.Context, jobID string, req JobDoneRequest) (*Job, error)
// StopAgent asks the agent backing runtimeID to terminate gracefully,
// without tearing down the surrounding runner-inner-child. The broker's
// `boid job done` call still fires normally, preserving any payload
// patch the agent wrote up to that point. NotifyTask calls this after
// `ApplyAction("ask")` so the awaiting transition does not race with
// payload_patch capture. No-op when runtimeID is empty.
StopAgent(runtimeID string)
}
type WorkspaceDetail ¶ added in v0.0.13
type WorkspaceDetail struct {
Slug string `json:"slug"`
Meta *orchestrator.WorkspaceMeta `json:"meta"`
Revision string `json:"revision,omitempty"`
// ProjectCount mirrors len(AssignedProjects); kept as its own field so
// callers that only need the count (e.g. a future list-style summary
// view) don't need to len() the slice themselves.
ProjectCount int `json:"project_count"`
AssignedProjects []string `json:"assigned_projects"`
// Home reports the workspace home directory's on-disk size (docs/plans/
// home-workspace-volume.md Phase 4 PR5). Populated only by GET
// /api/workspaces/{slug} (WorkspaceHandler.Show) — Create/Update/Import
// leave it nil, since computing it means walking a directory tree and
// none of those callers need it. nil (omitted from the JSON body) when
// the handler was not wired with a RuntimesDir to resolve it from.
Home *WorkspaceHomeSize `json:"home,omitempty"`
}
WorkspaceDetail is the response shape for the workspace create/show/update endpoints: the parsed meta plus enough bookkeeping (revision, assigned project ids) for a caller to display or re-PUT it with the correct If-Match header. docs/plans/workspace-db-consolidation.md Step C/D/E.
type WorkspaceHandler ¶
type WorkspaceHandler struct {
Service ProjectService
// RuntimesDir, when non-empty, is server/wire.go's runtimesDirFor(cfg) —
// used to resolve each workspace's home directory (docs/plans/
// home-workspace-volume.md Phase 4 PR5) for Show's size reporting and
// Remove's home-directory deletion. Left empty, both features degrade
// gracefully: Show omits WorkspaceDetail.Home, Remove reports
// home_deleted=false with no attempt made.
RuntimesDir string
}
func (*WorkspaceHandler) Create ¶ added in v0.0.13
func (h *WorkspaceHandler) Create(w http.ResponseWriter, r *http.Request)
Create handles POST /api/workspaces (docs/plans/workspace-db-consolidation.md PR4 Step C). The body is a yaml document with the target slug inlined (`slug: foo`) alongside the workspace meta fields — there is no URL parameter for the new slug, since the daemon does not yet know it.
func (*WorkspaceHandler) Export ¶ added in v0.0.13
func (h *WorkspaceHandler) Export(w http.ResponseWriter, r *http.Request)
Export handles GET /api/workspaces/{slug}/export (docs/plans/ workspace-db-consolidation.md PR5 Step A): the response body is the raw yaml the service returns verbatim (the marshaled WorkspaceMeta with a top-level "slug:" key inlined — the exact same shape POST /api/workspaces/import accepts, so an export → import round-trip needs no translation step — see ProjectAppService.ExportWorkspace's doc comment for the rationale). An ETag header mirrors the revision so a caller can round-trip it into a subsequent PUT's If-Match if it chooses that route instead of POST import.
func (*WorkspaceHandler) Import ¶ added in v0.0.13
func (h *WorkspaceHandler) Import(w http.ResponseWriter, r *http.Request)
Import handles POST /api/workspaces/import?mode=<create-only|replace> (docs/plans/workspace-db-consolidation.md PR5 Step B): the body shape mirrors Create's (top-level "slug:" key alongside the meta fields, decoded by the same DecodeWorkspaceCreateStrict). mode defaults to "create-only" (the safe choice — never overwrites an existing workspace) when the query param is omitted; an unrecognized mode value is rejected by ImportWorkspace itself with 400.
func (*WorkspaceHandler) List ¶
func (h *WorkspaceHandler) List(w http.ResponseWriter, r *http.Request)
func (*WorkspaceHandler) Remove ¶ added in v0.0.13
func (h *WorkspaceHandler) Remove(w http.ResponseWriter, r *http.Request)
Remove handles DELETE /api/workspaces/{slug} (Step F). The reserved default slug and re-assignment of any still-assigned project are enforced at the service/repository layer (ProjectAppService.RemoveWorkspace → orchestrator.WorkspaceRepository.Remove's transaction).
docs/plans/home-workspace-volume.md Phase 4 PR5 adds home directory deletion on top of the pre-existing row removal: the workspace row is always removed first (via h.Service.RemoveWorkspace), and only then does this attempt to delete slug's home directory on disk — trusted-side deletion, since a sandboxed job or a remote CLI client has no way to remove a path on the daemon's own filesystem itself. A home-deletion failure (e.g. permission denied) is reported in the response but does not turn this into an error response: the row is already gone, and the plan doc explicitly allows this "part-completed" outcome (see WorkspaceRemoveResponse's doc comment) rather than trying to make the two deletions atomic.
func (*WorkspaceHandler) Routes ¶
func (h *WorkspaceHandler) Routes() chi.Router
func (*WorkspaceHandler) Show ¶ added in v0.0.13
func (h *WorkspaceHandler) Show(w http.ResponseWriter, r *http.Request)
Show handles GET /api/workspaces/{slug} (Step D): meta + summary (revision, assigned project ids), with an ETag response header mirroring the revision so a client can round-trip it straight into a subsequent PUT's If-Match.
func (*WorkspaceHandler) Update ¶ added in v0.0.13
func (h *WorkspaceHandler) Update(w http.ResponseWriter, r *http.Request)
Update handles PUT /api/workspaces/{slug} (Step E): whole-document replace, gated by If-Match unless ?force=true is passed (decision 17 — PUT + If-Match, no PATCH). See ProjectAppService.UpdateWorkspace for the exact status code contract (428 missing If-Match, 412 stale If-Match).
type WorkspaceHomeSize ¶ added in v0.0.13
type WorkspaceHomeSize struct {
Slug string `json:"slug"`
// Path is the absolute on-disk path to the home directory, whether or
// not it currently exists.
Path string `json:"path"`
// Exists reports whether Path exists on disk. false means the workspace
// has never been dispatched into (docs/plans/home-workspace-volume.md's
// init-on-first-dispatch contract) — not an error.
Exists bool `json:"exists"`
// Bytes is the recursive apparent size of Path (humanize.ApparentSize —
// not a `du`-equivalent block-based size, see that function's doc
// comment), meaningful only when Exists is true and SizeError is empty.
Bytes int64 `json:"bytes"`
// Orphan is true when Path was found on disk but Slug has no
// corresponding workspace row — a workspace.yaml/DB row that was
// removed (or never assign/create'd) while its home directory survived.
// Only ever set by the GC listing (ListWorkspaceHomeSizes); the single-
// slug lookup used by GET /api/workspaces/{slug} always has a live
// workspace row (it is 404 otherwise), so it never carries Orphan=true.
Orphan bool `json:"orphan"`
// SizeError is non-empty when humanize.ApparentSize (or stat-ing Path in
// the first place) failed for a reason other than "does not exist" —
// typically a permission error, or a concurrent dispatch mutating the
// tree mid-walk. Bytes is 0 and must not be trusted when this is set;
// callers render "?" instead (docs/plans/home-workspace-volume.md PR5:
// "エラー時はエラーにせず「?」表示"). A non-empty SizeError does *not*
// imply the directory itself is gone or undeletable — deleteWorkspaceHome
// treats sizing as best-effort and still attempts deletion regardless
// (codex PR #791 review, Should-fix #2).
SizeError string `json:"size_error,omitempty"`
}
WorkspaceHomeSize describes one workspace home directory's on-disk footprint (docs/plans/home-workspace-volume.md Phase 4 PR5): used both by GET /api/workspaces/{slug} (a single entry, WorkspaceDetail.Home) and by POST /api/gc's workspace_homes listing (one entry per directory found under homes/).
func ListWorkspaceHomeSizes ¶ added in v0.0.13
func ListWorkspaceHomeSizes(runtimesDir string, lister WorkspaceSlugLister) (homes []WorkspaceHomeSize, listErr string, err error)
ListWorkspaceHomeSizes enumerates every directory under runtimesDir's sibling homes/ directory (docs/plans/home-workspace-volume.md's layout: homes/<slug>/, alongside sibling homes/<slug>.init.json and homes/<slug>.lock files that os.ReadDir's IsDir() filter excludes), and reports each one's size. Unlike computeWorkspaceHomeSize's single-slug lookup (driven by an already-known, already-validated slug), this walks the directory itself — so it also surfaces home directories with no corresponding workspace row at all ("orphans": docs/plans/ home-workspace-volume.md PR5: "workspace.yaml が消えて home だけ残った 孤児はレポートのみ"), which lister (when non-nil) is consulted to detect.
Orphan detection depends on a successful WorkspaceSlugLister.List call. A lister failure means orphan status is simply unknowable for every entry — rather than marking every entry Orphan=true (this function's pre-#791- review behavior, which silently misreported every real workspace's home as an "orphan" on a merely transient DB hiccup), a lister failure now omits the listing outright: homes comes back an empty (non-nil) slice, and listErr carries the lister's error message so a caller can render "size listing unavailable: <reason>" instead of trusting bogus per-entry orphan flags. This preserves the invariant "every WorkspaceHomeSize actually returned has a trustworthy Orphan flag" (codex PR #791 review, Should-fix #3, selection A — the plan's rejected alternative, selection B, would instead add a 3-state `orphan_known bool` per entry).
err (the third return) is reserved for a genuine directory-walk failure — an unresolvable runtimesDir, or os.ReadDir failing for a reason other than "does not exist yet". Unlike listErr, a non-nil err means the whole call failed, not just orphan detection; GC's own record-deletion work does not depend on workspace-home reporting succeeding, so callers should still treat a non-nil err as non-fatal to the rest of their own response.
Returns an empty (non-nil) slice, not an error, when homes/ does not exist yet (a fresh installation that has never dispatched a job).
type WorkspaceRemoveResponse ¶ added in v0.0.13
type WorkspaceRemoveResponse struct {
Status string `json:"status"`
// HomePath/HomeBytes describe the home directory as it was found right
// before the deletion attempt (empty/0 when RuntimesDir was not wired
// into the handler, or slug is the reserved default workspace). HomeBytes
// is only trustworthy when HomeSizeError is empty.
HomePath string `json:"home_path,omitempty"`
HomeBytes int64 `json:"home_bytes,omitempty"`
// HomeSizeError is non-empty when the daemon could not determine
// HomePath's on-disk size before attempting deletion (mirrors
// WorkspaceHomeSize.SizeError) — independent of whether the deletion
// itself subsequently succeeded. Split out from HomeDeleteError (codex
// PR #791 review, Should-fix #2): the two used to be conflated into a
// single field, so a caller could not tell "we don't know the size" (a
// diagnostic-only hiccup, e.g. a concurrent dispatch mutating the tree
// mid-walk) apart from "the directory is still there and undeletable" (a
// real part-completed-remove problem worth investigating).
HomeSizeError string `json:"home_size_error,omitempty"`
// HomeDeleted is true only when a home directory existed and os.RemoveAll
// on it succeeded. false covers every other case: no RuntimesDir wired,
// the default workspace's protected home, no home directory to begin
// with, or a deletion failure (see HomeDeleteError for that last one).
// Sizing is best-effort (see HomeSizeError) and never gates whether
// deletion is attempted — HomeDeleted can be true even when
// HomeSizeError is also non-empty.
HomeDeleted bool `json:"home_deleted"`
// HomeDeleteError is non-empty only when os.RemoveAll on the home
// directory itself failed. Never populated merely because sizing failed
// (see HomeSizeError for that).
HomeDeleteError string `json:"home_delete_error,omitempty"`
}
WorkspaceRemoveResponse is the response body for DELETE /api/workspaces/{slug} (docs/plans/home-workspace-volume.md Phase 4 PR5). The workspace row is always removed before any home-directory deletion is attempted (WorkspaceHandler.Remove calls Service.RemoveWorkspace first), so a non-empty HomeDeleteError (or HomeSizeError) reports a *partially* completed remove — deliberately allowed per the plan doc ("削除失敗... workspace 設定 (DB) の削除は先に完了させる (part-completed 状態を許容...)") rather than treated as a request failure: the response status stays 200.
type WorkspaceSlugLister ¶ added in v0.0.13
type WorkspaceSlugLister interface {
ListWorkspaces() ([]*orchestrator.WorkspaceSummary, error)
}
WorkspaceSlugLister exposes the set of currently known workspace slugs, used to flag orphaned home directories (a directory under homes/ with no corresponding workspace row). *ProjectAppService already satisfies this via its existing ListWorkspaces method — no new implementation is needed, only threading it through as this narrower interface at the handler construction site (server/wire.go) so GCHandler does not need the whole ProjectService surface.
type WorkspaceStore ¶ added in v0.0.13
type WorkspaceStore interface {
Load(slug string) (*orchestrator.WorkspaceMeta, error)
Save(slug string, meta *orchestrator.WorkspaceMeta) error
// Create is insert-only: an error wrapping os.ErrExist when slug already
// has a row (see orchestrator.WorkspaceRepository.Create).
Create(slug string, meta *orchestrator.WorkspaceMeta) error
Remove(slug string) error
// LoadWithRevision returns meta and its revision from a single atomic
// snapshot (docs/plans/workspace-db-consolidation.md MAJOR 1, codex
// review), used by GET /api/workspaces/{slug} so meta and revision can
// never straddle a concurrent write. See
// orchestrator.WorkspaceRepository.LoadWithRevision's doc comment.
LoadWithRevision(slug string) (*orchestrator.WorkspaceMeta, string, error)
// UpdateIfRevisionMatches performs a compare-and-swap update: meta is
// written only if slug's current revision equals expectedRevision,
// atomically with the check. matched=false covers both "no such slug"
// and "revision mismatch" — see
// orchestrator.WorkspaceRepository.UpdateIfRevisionMatches's doc comment.
UpdateIfRevisionMatches(slug string, expectedRevision string, meta *orchestrator.WorkspaceMeta) (newRevision string, matched bool, err error)
}
WorkspaceStore provides direct CRUD over a single workspace's WorkspaceMeta, independent of the project-assignment bookkeeping that lives on ProjectRepository below (docs/plans/workspace-db-consolidation.md PR4). Implemented by *orchestrator.WorkspaceStore (via ProjectStore.WorkspaceStore()), wired in internal/server/wire.go.
Source Files
¶
- action.go
- attachment_read_linux.go
- attachments.go
- blocking_ask_registry.go
- broker.go
- config.go
- device_auth.go
- events_handler.go
- gc.go
- helpers.go
- host_commands.go
- job.go
- job_log_sse.go
- job_model.go
- payload_patch_lock.go
- project.go
- project_service.go
- secret.go
- service.go
- session.go
- store.go
- task.go
- task_ask.go
- task_context.go
- task_create.go
- task_event_hub.go
- task_field.go
- task_notify.go
- task_service.go
- tree.go
- web.go
- web_service.go
- workflow.go
- workflow_action.go
- workflow_job.go
- workflow_replay.go
- workspace.go
- workspace_homes.go
- ws_attach.go