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
- func ReadOwnerConfig(path string) ([]byte, error)
- type ArtifactInfo
- type Config
- type EligibleRead
- type Error
- type LaunchPolicy
- type ListRequest
- type Policy
- type ReadRequest
- type ReadResult
- type ResponderPolicy
- type Service
- func (s *Service) CallTool(name string, raw json.RawMessage) (map[string]any, bool)
- func (s *Service) Instructions() string
- func (s *Service) List(req ListRequest) (map[string]any, error)
- func (s *Service) Read(req ReadRequest) (*ReadResult, error)
- func (s *Service) ReadInventory() ([]EligibleRead, error)
- func (s *Service) TaskArtifacts(agent *piagent.Agent, task *piagent.TaskView) []map[string]any
- func (s *Service) Tools() []piagent.Tool
- func (s *Service) Upload(req UploadRequest) (map[string]any, error)
- type UploadRequest
Constants ¶
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 )
const ( ToolList = "devx_artifact_list" ToolRead = "devx_artifact_read" ToolUpload = "devx_attachment_upload" )
Variables ¶
This section is empty.
Functions ¶
func ReadOwnerConfig ¶
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"`
// 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 ¶
Error is a client-facing error. Messages never contain absolute paths or file contents.
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 ¶
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 ¶
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 ¶
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 ¶
New returns a Service backed by the real DevX session metadata and the given policy source.
func (*Service) CallTool ¶
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 ¶
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 ¶
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.