artifactbridge

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package artifactbridge lets remote MCP clients exchange explicitly registered files with a managed Pi agent's DevX session, without any general filesystem access.

Eligibility: only files registered in the session's existing DevX artifact manifest (<worktree>/.artifacts/manifest.json) can be listed or read, and uploads land only in <worktree>/.artifacts/attachments/ and are registered in that same manifest. There is no parallel registry: retention, archive and removal remain the existing `devx artifact` semantics.

Trust boundary (read this before granting access)

`devx mcp pi` receives NO caller identity. The Agent Shed gateway authenticates the calling principal (assistant), checks that principal's reviewed grant for the relay app (which tools), and passes only the tool name and arguments; the relay forwards them over stdio. The agent_id in the arguments is chosen by the caller. So this package cannot prove that a given caller may read a given session; it can only enforce what the local owner has opted in to. Concretely, any principal granted a bridge tool on the devx-pi app can use it on every session the owner exposed, and nothing more.

The local scope is the owner's policy (policy.go), re-read from the owner's global DevX config and re-evaluated on every tools/list and tool call, so edits take effect on the next call with no restart:

  • read (default on): every session available through the MCP path in pi_mcp.allowed_projects, unless pi_mcp.artifacts.sessions is present, in which case only those sessions; exclude_sessions/exclude_projects always subtract;
  • upload (default off): only sessions named in an explicit pi_mcp.artifacts.sessions list, never the default-wide read scope;
  • in every case the agent must be the unambiguous owner of a live session (eligibleSession): not retired, session exists at the agent's worktree with the same project, no marker naming another agent, no duplicate claimant or shared worktree. A missing ownership marker is not a denial (the MCP path does not require one); a contradictory one always is.

An unreadable or invalid config denies everything for that call.

Artifact IDs are opaque and derived from (agent, session, manifest id), so an ID observed in one session does not resolve in any other. They are not secrets or capabilities: possession of an ID grants nothing.

Index

Constants

View Source
const (

	// AttachmentsFolder is the artifact folder that receives uploads.
	AttachmentsFolder = "attachments"
	// AttachmentTag marks manifest entries created by remote upload.
	AttachmentTag = "remote-attachment"

	// MaxReadableBytes bounds files that can be hashed or read.
	MaxReadableBytes = 32 << 20
	// MaxReadChunk bounds one read chunk (raw bytes).
	MaxReadChunk = 16 << 10
	// MaxInlineImageBytes bounds an image returned as an MCP image block.
	MaxInlineImageBytes = 40 << 10
	// MaxResultBytes keeps every tool result under the Agent Shed gateway's
	// 64 KiB result bound with headroom for the JSON-RPC envelope.
	MaxResultBytes = 60000
	// MaxUploadChunk bounds one decoded upload chunk so the base64 argument
	// stays under the gateway's 16 KiB argument bound.
	MaxUploadChunk = 8 << 10
	// DefaultMaxUploadBytes / MaxUploadBytesCeiling bound one attachment.
	DefaultMaxUploadBytes = 10 << 20
	MaxUploadBytesCeiling = 25 << 20
	// MaxPendingUploads bounds incomplete uploads per agent.
	MaxPendingUploads = 8
	// UploadExpiry is how long an incomplete upload's staged bytes are kept
	// without progress. Completed attachments are never expired here.
	UploadExpiry = 24 * time.Hour
	// CompletedRecordExpiry is how long a finished upload's completion record
	// (used to replay its result) is kept in DevX state.
	CompletedRecordExpiry = 30 * 24 * time.Hour
	// MaxSessionAttachments / MaxSessionAttachmentBytes bound the remote
	// attachments registered in one session, so a granted principal cannot
	// fill the worktree's disk with fresh idempotency keys.
	MaxSessionAttachments     = 200
	MaxSessionAttachmentBytes = 512 << 20
	// MaxListPage bounds one list page.
	MaxListPage = 50
	// MaxTaskArtifacts bounds artifacts attached to a task status.
	MaxTaskArtifacts = 20
)
View Source
const (
	ToolList   = "devx_artifact_list"
	ToolRead   = "devx_artifact_read"
	ToolUpload = "devx_attachment_upload"
)

Variables

This section is empty.

Functions

func ReadOwnerConfig

func ReadOwnerConfig(path string) ([]byte, error)

ReadOwnerConfig reads the owner's DevX config file for security-sensitive policy (the artifact bridge policy and the managed-agent launch policy). A missing file is reported as an error wrapping os.ErrNotExist so callers can apply their defaults. The file must be a regular file of bounded size and, where the bridge runs (darwin/linux), owned by the current user and not writable by group or others.

Types

type ArtifactInfo

type ArtifactInfo struct {
	ID         string            `json:"id"`
	ManifestID string            `json:"manifest_id"`
	Title      string            `json:"title"`
	Type       string            `json:"type"`
	MIMEType   string            `json:"mime_type"`
	Size       int64             `json:"size,omitempty"`
	Version    string            `json:"version,omitempty"`
	Checksums  map[string]string `json:"checksums,omitempty"`
	Created    time.Time         `json:"created"`
	Retention  string            `json:"retention"`
	Folder     string            `json:"folder,omitempty"`
	Tags       []string          `json:"tags,omitempty"`
	Summary    string            `json:"summary,omitempty"`
	Attachment bool              `json:"attachment,omitempty"`
	Available  bool              `json:"available"`
	// Unavailable explains why an entry cannot be read (missing file,
	// symlink, too large); it never contains a path.
	Unavailable string `json:"unavailable,omitempty"`
}

ArtifactInfo is the remote-facing description of a registered artifact.

type Config

type Config struct {
	// StateDir holds upload staging state (outside every worktree).
	StateDir string
}

Config holds the bridge's static settings.

type EligibleRead

type EligibleRead struct {
	AgentID string `json:"agent_id"`
	Session string `json:"session"`
	Project string `json:"project"`
	Allowed bool   `json:"allowed"`
	Reason  string `json:"reason,omitempty"`
}

EligibleRead reports, for every agent record, whether default artifact reads would be allowed now and why not. It is an inventory/diagnostic helper with the same predicate and policy as authorize; it serves no data.

type Error

type Error struct {
	Code  string
	Msg   string
	Extra map[string]any
}

Error is a client-facing error. Messages never contain absolute paths or file contents.

func (*Error) Error

func (e *Error) Error() string

type LaunchPolicy

type LaunchPolicy struct {
	AllowedProjects []string
	PiCommand       string
	PiArgs          []string
	PassEnv         []string
}

LaunchPolicy is the owner's managed-agent launch policy: which projects pi_start_task may start agents in, and how Pi is launched. It comes only from the owner's DevX config file (see ReadOwnerConfig), never from a project .devx/config.yaml or DEVX_* environment variables: a repository must not be able to widen the start allowlist or choose the executable, arguments or environment of a managed agent.

pi_mcp:
  allowed_projects: [...]   # shared with the artifact bridge
  pi_command: pi            # optional
  pi_args: [...]            # optional
  pass_env: [NAME, ...]     # optional

func LoadLaunchPolicyFile

func LoadLaunchPolicyFile(path string) (LaunchPolicy, error)

LoadLaunchPolicyFile reads the launch policy from the owner's config file. A missing file yields the zero policy (no allowed projects, so every start is denied). Any read, trust, parse or validation problem is an error.

func ParseLaunchPolicy

func ParseLaunchPolicy(data []byte) (LaunchPolicy, error)

ParseLaunchPolicy extracts and strictly validates pi_mcp.allowed_projects, pi_command, pi_args and pass_env. Other keys (including pi_mcp.artifacts) are ignored here. Wrong types, nulls, empty names, duplicate keys, aliases and merge keys on the paths read are errors.

type ListRequest

type ListRequest struct {
	AgentID string
	Type    string
	Search  string
	Cursor  int
	Limit   int
}

ListRequest selects artifacts of one agent's session.

type Policy

type Policy struct {
	Read            bool
	Upload          bool
	AllowedProjects []string
	// Sessions is the explicit allowlist; SessionsSet reports whether the
	// key was present (an explicit empty list exposes nothing).
	Sessions        []string
	SessionsSet     bool
	ExcludeSessions []string
	ExcludeProjects []string
	MaxUploadBytes  int64
}

Policy is the owner's artifact-bridge policy. It is re-read and validated on every tools/list and tool call (see Service.policy), so edits to the owner config take effect on the next call without restarting `devx mcp pi` or the relay. A config that cannot be read or does not validate disables the bridge for that call (fail closed).

pi_mcp:
  allowed_projects: [...]      # shared with pi_start_task
  artifacts:
    read: true                 # default true
    upload: false              # default false
    sessions: [a, b]           # optional restrictive allowlist (present, even empty = only these)
    exclude_sessions: [c]      # optional, always subtracted
    exclude_projects: [p]      # optional, always subtracted
    max_upload_bytes: 10485760 # optional

Read eligibility, when `sessions` is absent, is every MCP-visible managed session (a live managed agent whose DevX session record still belongs to it) in an allowed project, minus exclusions. Upload eligibility never uses that default: it requires upload: true AND an explicit `sessions` list naming the session.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy applies when the config file has no pi_mcp.artifacts block. With no allowed projects nothing is eligible.

func LoadPolicyFile

func LoadPolicyFile(path string) (Policy, error)

LoadPolicyFile reads the policy from one DevX config file. A missing file yields DefaultPolicy (which has no allowed projects, so nothing is eligible). Any read, parse or validation problem is an error.

func ParsePolicy

func ParsePolicy(data []byte) (Policy, error)

ParsePolicy extracts and strictly validates pi_mcp.allowed_projects and pi_mcp.artifacts from a DevX config document. Other keys are ignored. Within pi_mcp.artifacts, unknown keys, null values, wrong types, aliases and duplicate keys are errors: a typo such as `session:` must never fall back to the default-wide scope.

type ReadRequest

type ReadRequest struct {
	AgentID    string
	ArtifactID string
	Version    string
	Offset     int64
	MaxBytes   int
	// Encoding: auto (default), text, base64 or image.
	Encoding string
}

ReadRequest reads part of one artifact.

type ReadResult

type ReadResult struct {
	Artifact    ArtifactInfo `json:"artifact"`
	Encoding    string       `json:"encoding"`
	Offset      int64        `json:"offset"`
	Length      int          `json:"length"`
	NextOffset  int64        `json:"next_offset"`
	EOF         bool         `json:"eof"`
	ChunkSHA256 string       `json:"chunk_sha256"`
	Text        *string      `json:"text,omitempty"`
	Base64      *string      `json:"data_base64,omitempty"`
	// contains filtered or unexported fields
}

ReadResult is the metadata part of a read; the payload travels in Text, Base64 or Image.

type ResponderPolicy

type ResponderPolicy struct {
	Enabled  bool
	Mode     string
	Command  string
	Args     []string
	Timeout  time.Duration
	ReadOnly bool
}

ResponderPolicy is the owner's `devx ask` responder policy: whether a local command answers asks, when, and which executable and arguments it runs. It comes only from the owner's DevX config file (see ReadOwnerConfig), never from a project .devx/config.yaml or DEVX_* environment variables: a repository must not be able to enable the responder, switch it to run without approval, or choose what it executes.

agent_responder:
  enabled: true
  mode: approval   # none | approval | always
  command: pi
  args: [...]
  timeout: 5m
  read_only: true

func DefaultResponderPolicy

func DefaultResponderPolicy() ResponderPolicy

DefaultResponderPolicy is used for keys the owner config leaves unset.

func LoadResponderPolicyFile

func LoadResponderPolicyFile(path string) (ResponderPolicy, error)

LoadResponderPolicyFile reads the responder policy from the owner's config file. A missing file yields the defaults (responder disabled). Any read, trust, parse or validation problem is an error; callers fail closed.

func ParseResponderPolicy

func ParseResponderPolicy(data []byte) (ResponderPolicy, error)

ParseResponderPolicy strictly validates agent_responder. Unset keys take DefaultResponderPolicy values. Wrong types, nulls, unknown keys, duplicate keys, aliases and merge keys are errors.

type Service

type Service struct {
	Cfg    Config
	Agents *piagent.Store
	// Sessions returns the current DevX session records.
	Sessions func() (map[string]*session.Session, error)
	// Policy returns the current owner policy; it is called for every
	// tools/list and tool call. An error disables the bridge for that call.
	Policy func() (Policy, error)
	Now    func() time.Time
}

Service implements the bridge operations.

func New

func New(cfg Config, agents *piagent.Store, policy func() (Policy, error)) *Service

New returns a Service backed by the real DevX session metadata and the given policy source.

func (*Service) CallTool

func (s *Service) CallTool(name string, raw json.RawMessage) (map[string]any, bool)

CallTool routes the bridge tools. A bridge tool whose capability is off, or any bridge tool while the policy is unreadable, is answered with an error (never executed); non-bridge names are not handled here.

func (*Service) Instructions

func (s *Service) Instructions() string

Instructions describes the artifact tools when any are enabled.

func (*Service) List

func (s *Service) List(req ListRequest) (map[string]any, error)

List returns registered artifacts, newest first.

func (*Service) Read

func (s *Service) Read(req ReadRequest) (*ReadResult, error)

Read returns one bounded chunk of a registered artifact, verified against the requested version, plus a full-content checksum from the same pass.

func (*Service) ReadInventory

func (s *Service) ReadInventory() ([]EligibleRead, error)

ReadInventory evaluates read eligibility for every agent record.

func (*Service) TaskArtifacts

func (s *Service) TaskArtifacts(agent *piagent.Agent, task *piagent.TaskView) []map[string]any

TaskArtifacts returns artifacts registered in the agent's session while the task ran (from delivery to finish, plus a short grace period), excluding remote attachments. It returns nil when reading is disabled, the agent is not authorized, or the task was never delivered.

func (*Service) Tools

func (s *Service) Tools() []piagent.Tool

Tools lists the capabilities enabled by the current policy (re-read on every tools/list). Definitions are constant text so a policy change never alters an approved tool definition; an unreadable policy lists nothing.

func (*Service) Upload

func (s *Service) Upload(req UploadRequest) (map[string]any, error)

Upload processes one chunk (or a progress probe) of an attachment upload.

type UploadRequest

type UploadRequest struct {
	AgentID        string
	IdempotencyKey string
	Filename       string
	MIMEType       string
	Size           int64
	SHA256         string
	Title          string
	Summary        string
	Offset         int64
	DataBase64     *string
}

UploadRequest is one upload call.

Jump to

Keyboard shortcuts

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