agent

package
v1.55.0 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Index

Constants

View Source
const (
	GateRoleEnvVar       = "NS_GATE"
	LegacyGateRoleEnvVar = "NO_MISTAKES_GATE"
	GateStepKindEnvVar   = "MO_GATE_STEP_KIND"
	GateTurnKindEnvVar   = "MO_GATE_TURN_KIND"
)

GateRoleEnvVar is exported into every spawned gate agent's environment as an coarse diagnostic marker that the process is a no-slop gate agent (a review/fix/document/test/lint/rebase/pr/ci invocation), NOT a fleet operator. It is defense in depth only: it can be removed, forged, or inherited, so runtime authorization uses canonical managed Git identity plus authenticated daemon peer process ancestry. Its purpose is containment: when the target repository is itself an agent-orchestration harness (for example firstmate), the target's project agent-instruction file can otherwise convince the gate agent it is the fleet captain and drive it to spawn a crew and reset the shared branch it is validating (see the ambient-authority incident). A cooperating harness reads this marker and its fleet-lifecycle entrypoints fail closed. It is deliberately coarse (`=1`): presence is the whole signal.

View Source
const (
	// LifecyclePhaseStart marks native subprocess startup.
	LifecyclePhaseStart = "start"
	// LifecyclePhaseExit marks native subprocess exit.
	LifecyclePhaseExit = "exit"
	// LifecyclePhaseRetry marks a transient retry before the next subprocess attempt.
	LifecyclePhaseRetry = "retry"
)
View Source
const (
	ServerPIDOwnerDaemon = "daemon"
	ServerPIDOwnerWizard = "wizard"
)

Variables

This section is empty.

Functions

func CurrentProcessStartedAt

func CurrentProcessStartedAt() time.Time

func CurrentServerPIDOwner

func CurrentServerPIDOwner() string

func CurrentServerPIDsDir

func CurrentServerPIDsDir() string

CurrentServerPIDsDir returns the configured directory for PID tracking files, or "" if tracking is disabled.

func EnsureGateNeutralized

func EnsureGateNeutralized(a Agent) error

EnsureGateNeutralized fails closed when the agent that will run gate steps in the target checkout does not neutralize that checkout's project agent-instruction files. Callers must invoke it before launching any gate agent so an unverified harness is refused with a clear error rather than run unneutralized in the target checkout. Only codex, claude, and pi have a verified neutralization knob today.

func FreshInputTokens

func FreshInputTokens(inputTokens, cacheReadTokens int) int

FreshInputTokens is the non-cached portion of an invocation's reported input: the input tokens that were not served from the provider's prompt cache. It is the honest per-invocation cost signal, separated from cache reads. It never goes negative.

func IsAgentUnavailable

func IsAgentUnavailable(err error) bool

IsAgentUnavailable reports whether an invocation failed because the agent lane could not serve it, rather than because the work the agent was asked to assess produced a negative result. Agent adapters return successful Result values for structured findings; launch failures, provider outages, timeouts, and process exits arrive as errors.

This is the single availability classifier used both by fallback routing and by callers that must preserve an independently measured result when a later narration invocation dies. Keep quota recognition structural through IsQuotaOutage: matching its rendered text here would recreate the competing outage classifier this package exists to avoid.

func IsGateDutyEnv added in v1.55.0

func IsGateDutyEnv(entry string) bool

func IsQuartermasterRefusal

func IsQuartermasterRefusal(err error) bool

func IsQuotaOutage

func IsQuotaOutage(err error) bool

IsQuotaOutage reports whether err says provider quota exhaustion is what failed the invocation: a single lane's *LaneOutageError (skipped while marked, or freshly classified from the provider banner) or the fallback wrapper's every-eligible-lane aggregate. Telemetry classification keys on this instead of substring-matching error text, which embeds provider banner excerpts like "codex exited: ..." and would misfile the outage.

func LaneName

func LaneName(name types.AgentName) string

LaneName returns the key a configured agent name's lane health is recorded under: the identity the constructed agent reports from Agent.Name(), which for every ACP-driven agent is its target rather than the alias the operator configured. Read surfaces must resolve through this so they cannot look up a lane the pipeline never writes.

func ModelTimeMS

func ModelTimeMS(durationMS, subprocessWaitMS int64) int64

ModelTimeMS is the authoritative split of invocation wall-clock into model/reasoning time: the invocation duration minus the time spent waiting on tool subprocesses. It never goes negative.

func NeutralizesGateInstructions

func NeutralizesGateInstructions(a Agent) bool

NeutralizesGateInstructions reports whether a (possibly wrapped) agent neutralizes the target repo's project agent-instruction files during a gate run. It fails closed: an adapter that does not implement the capability, a nil agent, or a wrapper any member of which does not neutralize, is reported as NOT neutralized.

func PerRoundTokens

func PerRoundTokens(current, priorCumulative int, cumulative bool) int

PerRoundTokens converts a token counter into the per-round amount for one invocation. Some adapters (codex) report usage cumulatively across a resumed durable session, so round N's raw counter includes rounds 1..N-1; there the per-round amount is current minus the same session's previous cumulative. Adapters that report per-invocation usage (cumulative == false), and the first invocation of any session (priorCumulative <= 0), report current as-is. A cumulative counter that appears to shrink is treated as non-cumulative for that row rather than fabricating a negative or oversized delta.

func QuartermasterPoolForLane

func QuartermasterPoolForLane(lane string) (string, bool)

func ReportsAgentAttempts

func ReportsAgentAttempts(a Agent) bool

ReportsAgentAttempts reports whether a (possibly wrapped) agent emits an Attempt callback for each concrete adapter attempt.

func SetManagedServerOutput

func SetManagedServerOutput(w io.Writer)

SetManagedServerOutput routes future managed-server stdout/stderr to w. Passing nil resets to the default (os.Stderr). Only affects servers started after this call; already-running servers keep their original fds.

func SetServerPIDsDir

func SetServerPIDsDir(dir string)

SetServerPIDsDir configures where managed-server PID files are written. Callers (typically the daemon at startup) should point this at paths.ServerPIDsDir(). Empty string disables PID tracking, which is the default for processes that don't own a long-running daemon identity.

func SetServerPIDsDirForOwner

func SetServerPIDsDirForOwner(dir, owner string)

func SuggestBranchAndCommit

func SuggestBranchAndCommit(ctx context.Context, ag Agent, dir string) (branch, subject string, err error)

SuggestBranchAndCommit asks the agent to propose both a git branch name and a conventional commit subject for the current working-tree state in a single call. Combining the two saves one full agent round-trip when the wizard needs both (new branch + dirty tree).

The branch name must be present and sanitizes to a valid git ref; otherwise an error is returned. The commit subject is best-effort: if the agent returns an empty subject, this function returns an empty string with no error so the caller can fall back to SuggestCommitMessage.

func SuggestBranchName

func SuggestBranchName(ctx context.Context, ag Agent, dir string) (string, error)

SuggestBranchName asks the agent to propose a short git branch name for the current working-tree state in dir. The suggestion is sanitized so it's safe to pass to `git checkout -b`.

func SuggestCommitMessage

func SuggestCommitMessage(ctx context.Context, ag Agent, dir string) (string, error)

SuggestCommitMessage asks the agent to propose a single-line commit subject summarizing the current working-tree state at dir.

func SupportsSessionProvider

func SupportsSessionProvider(a Agent, provider string) bool

SupportsSessionProvider reports whether a (possibly wrapped) agent can resume a session minted by provider.

func SupportsSessionResume

func SupportsSessionResume(a Agent) bool

SupportsSessionResume reports whether a (possibly wrapped) agent can start and resume durable native sessions.

func WorktreeSteering

func WorktreeSteering(evidenceRoot string) string

WorktreeSteering renders the preamble prepended to every pipeline agent prompt. It keeps the agent's writes inside the git worktree and steers it away from mutating system state outside the workspace (installing/upgrading system packages, modifying apps in /Applications, changing global config). Those out-of-tree writes are what trigger macOS "App Management" / Privacy notifications and risk surprising side effects on the user's machine.

evidenceRoot is the one out-of-worktree location the preamble permits, and it is supplied by the caller rather than computed here. This used to be a package-level string that rebuilt the evidence path from os.TempDir() independently of the test step - two copies of one fact, either of which could move without the other. The caller owns the path; this owns the wording.

Types

type Agent

type Agent interface {
	Name() string
	Run(ctx context.Context, opts RunOpts) (*Result, error)
	Close() error
}

Agent is the interface for running AI agent tasks.

func New

func New(name types.AgentName, bin string, extraArgs []string) (Agent, error)

New creates an agent by name with the given binary path. For native agents, extraArgs are user CLI flags from agent_args_override that are injected into the underlying tool's argv ahead of no-slop' managed flags. ACP agents and aliases ignore extraArgs; use NewWithOptions to provide registry overrides.

func NewFallback

func NewFallback(agents []Agent) Agent

NewFallback returns an Agent that tries each agent in order when an invocation fails because the current agent process is unavailable.

func NewNoop

func NewNoop() Agent

NewNoop returns an agent that does nothing. Used for demo mode where mock steps handle all logic without calling a real agent.

func NewWithOptions

func NewWithOptions(name types.AgentName, bin string, extraArgs []string, opts Options) (Agent, error)

NewWithOptions creates an agent by name with additional backend-specific options.

func WithLaneHealth

func WithLaneHealth(a Agent, store LaneHealthStore, now func() time.Time) Agent

WithLaneHealth wraps a single agent lane with persisted quota-outage tracking. A nil store returns the agent unchanged, so demo mode and tests that do not care keep the previous behavior exactly.

func WithQuartermasterLease

func WithQuartermasterLease(a Agent, opts QuartermasterOptions) Agent

func WithSteering

func WithSteering(a Agent, evidenceRoot string) Agent

WithSteering wraps an agent so every invocation is steered to keep writes inside the worktree, naming evidenceRoot as the one permitted out-of-worktree destination. Wrapping is idempotent: an already-steered agent is returned unchanged so the preamble is never added twice.

type Attempt

type Attempt struct {
	Agent           string
	Identity        InvocationIdentity
	Result          *Result
	Err             error
	StartedAt       time.Time
	CompletedAt     time.Time
	Session         *SessionRef
	SessionFallback bool
}

Attempt describes one completed concrete adapter attempt for an agent invocation. An Agent may make several attempts when it retries transient failures or moves to a fallback provider.

type AttemptReporter

type AttemptReporter interface {
	ReportsAgentAttempts() bool
}

AttemptReporter is the optional adapter capability for reporting every concrete attempt, including internal retries and fallback providers.

type GateInstructionNeutralizer

type GateInstructionNeutralizer interface {
	NeutralizesGateInstructions() bool
}

GateInstructionNeutralizer is the optional adapter capability that reports the adapter neutralizes the target repository's project agent-instruction files (AGENTS.md/CLAUDE.md) for this invocation, so they cannot install a governing identity on the gate agent.

A gate agent runs with cmd.Dir set to the target checkout and a free shell. If the checkout is itself an agent-orchestration harness (for example firstmate), its AGENTS.md can otherwise convince the gate agent it is the fleet captain and drive it to spawn a crew and reset the branch it is validating (the ambient-authority incident). Only adapters whose suppression knob is empirically verified implement this and return true, and only while that knob is actually in effect for the invocation (an operator override that defeats the knob must report false so the gate fails closed rather than launching unneutralized).

type InvocationIdentity

type InvocationIdentity struct {
	ConfiguredAgent string
	Executable      *string
	ModelArgs       []string
}

InvocationIdentity is observed launch identity for one concrete agent adapter. ModelArgs contains only model/provider-selecting argv entries; it never contains the prompt, environment, or unrelated arguments.

func ResolveInvocationIdentity

func ResolveInvocationIdentity(a Agent) InvocationIdentity

ResolveInvocationIdentity returns the launch identity an adapter can observe. Missing capabilities stay unknown rather than being guessed from Agent.Name.

type InvocationIdentityReporter

type InvocationIdentityReporter interface {
	InvocationIdentity() InvocationIdentity
}

InvocationIdentityReporter is implemented by executable-backed adapters and by decorators that can forward the underlying adapter's identity.

type InvocationMetrics

type InvocationMetrics struct {
	// ModelRoundtrips counts the model-authored items in the turn (assistant
	// messages plus tool calls). It is a live-stream proxy for productive model
	// round-trips: because codex batches an exec into a single turn and does not
	// surface internal poll round-trips as items, every counted item is
	// productive work, not "are-we-there-yet" polling.
	ModelRoundtrips int
	// ToolCalls counts whole tool invocations (one command_execution item is one
	// tool call regardless of how many sub-commands it chains).
	ToolCalls int
	// ToolCategories is the per-sub-command histogram (see ToolCategoryCounts).
	ToolCategories ToolCategoryCounts
	// SubprocessWaitMS is the wall-clock spent inside tool subprocesses,
	// measured by the reader as the sum of each tool item's started->completed
	// interval. Combined with the invocation duration it separates subprocess
	// wait from model/reasoning time (see ModelTimeMS).
	SubprocessWaitMS int64
}

InvocationMetrics is the bounded activity evidence an adapter extracts from one invocation's event stream. A nil *InvocationMetrics means the adapter reported nothing (recorded as NULL, never a fabricated zero); a non-nil value means every field is meaningful, including a genuine zero.

type InvocationWorkload

type InvocationWorkload struct {
	Files int
	Lines int
}

InvocationWorkload is the bounded size of the change an invocation works over: changed files and net changed lines. It carries no paths or content.

type LaneHealthStore

type LaneHealthStore interface {
	Outage(lane string) (lanehealth.Outage, bool)
	ClaimProbe(lane string) bool
	Mark(outage lanehealth.Outage) error
	ClearObservedBefore(lane string, startedAt time.Time) error
}

LaneHealthStore is the slice of lanehealth.Store this package needs, kept as an interface so tests and future callers can substitute their own.

type LaneOutageError

type LaneOutageError struct {
	Lane   string
	Until  time.Time
	Reason string
	// contains filtered or unexported fields
}

LaneOutageError reports that one agent lane cannot run because the provider's quota is exhausted until Until. It wraps the provider failure that produced the mark when there is one, so the original banner still reaches the step log.

func (*LaneOutageError) Error

func (e *LaneOutageError) Error() string

func (*LaneOutageError) Unwrap

func (e *LaneOutageError) Unwrap() error

type LifecycleEvent

type LifecycleEvent struct {
	Agent   string
	Phase   string
	PID     int
	Message string
}

LifecycleEvent describes process-level activity for an agent invocation. The pipeline records these as step log lines and active-step heartbeats.

type Options

type Options struct {
	ACPRegistryOverrides map[string]string
	// DisableProjectSettings, when true, asks a supported adapter (codex,
	// claude, pi) to launch with the target repo's project-level agent
	// settings/instructions suppressed. It is the resolved, trusted-only opt-out
	// from config.Config; adapters without a verified suppression knob ignore it
	// and are refused separately by EnsureGateNeutralized when the opt-out is on.
	DisableProjectSettings bool
}

Options configures backend-specific agent construction behavior. ACPRegistryOverrides maps acpx target names, including first-class alias targets, to raw ACP agent commands.

type OutputParseError

type OutputParseError struct {
	Agent   string
	Snippet string
	// contains filtered or unexported fields
}

OutputParseError reports that an agent's own final message could not be parsed against the requested schema. It is the one adapter failure whose text quotes AGENT-AUTHORED output rather than the provider's stderr or structured error channel, so callers that classify provider text - notably the lane-health quota classifier - must recognize it and skip it. A reviewed repository that merely mentions a quota banner would otherwise be able to park a healthy lane.

func (*OutputParseError) Error

func (e *OutputParseError) Error() string

func (*OutputParseError) Unwrap

func (e *OutputParseError) Unwrap() error

type QuartermasterAcquireRequest

type QuartermasterAcquireRequest struct {
	Pool    string
	Holder  string
	Purpose string
	TTL     time.Duration
	Weight  int
}

type QuartermasterClient

type QuartermasterClient interface {
	Acquire(context.Context, QuartermasterAcquireRequest) (QuartermasterLease, error)
	Release(context.Context, QuartermasterLease, string) error
}

func NewCommandQuartermasterClient

func NewCommandQuartermasterClient(bin string) QuartermasterClient

type QuartermasterLease

type QuartermasterLease struct {
	Account string
	ID      string
	Until   time.Time
	Pool    string
	Holder  string
}

type QuartermasterOptions

type QuartermasterOptions struct {
	Client QuartermasterClient
	Pool   string
	Holder string
	TTL    time.Duration
	Weight int
	Home   string
}

type QuartermasterRefusalError

type QuartermasterRefusalError struct {
	Pool    string
	Purpose string
	Reason  string
	Cause   error
}

QuartermasterRefusalError reports that no usable lease was obtained. Cause carries the underlying subprocess failure when the refusal was not a policy decision at all - a missing binary, a timeout, a non-zero exit - so callers can tell "the authority said no" from "the authority could not be reached".

func (*QuartermasterRefusalError) Error

func (e *QuartermasterRefusalError) Error() string

func (*QuartermasterRefusalError) Unwrap

func (e *QuartermasterRefusalError) Unwrap() error

type Result

type Result struct {
	// Output is structured JSON returned by the agent. Text-parsed fallback
	// results are validated before return, and optional fields may be
	// nullable there.
	Output json.RawMessage
	// Text is the raw text output.
	Text string
	// Usage tracks token consumption for the invocation.
	Usage         TokenUsage
	UsageReported bool
	// SessionID is the adapter-native session identity of this invocation
	// when the adapter reports one. Callers persist it to resume later.
	SessionID string
	// Resumed reports whether this invocation resumed opts.Session.ID.
	Resumed bool
	// Model is the model the adapter reported serving this invocation, when
	// available. Instrumentation only.
	Model string
	// ModelProvider is the provider that served the model (e.g. "openai",
	// "anthropic"), when the adapter can report it. Instrumentation only.
	ModelProvider string
	// Provider is the adapter provider that served this invocation. It lets
	// fallback wrappers persist a session against the provider that minted it.
	Provider string
	// Metrics is the bounded per-invocation activity evidence the adapter
	// extracted from its event stream (round-trips, tool calls + categories,
	// subprocess wait time). Nil means the adapter reported nothing, which is
	// recorded as unknown (NULL) rather than a fabricated zero.
	Metrics *InvocationMetrics
	// CacheCreationReported reports whether Usage.CacheCreationTokens is a
	// meaningful value. Adapters whose provider does not surface cache-creation
	// cost (codex) leave it false so the field is recorded as unknown instead of
	// a fabricated zero.
	CacheCreationReported bool
	// SessionUsageCumulative reports that Usage accumulates across a resumed
	// durable session, so round N's counters include rounds 1..N-1. The pipeline
	// uses it to record correct per-round token deltas (see PerRoundTokens).
	SessionUsageCumulative bool
}

Result holds the output of an agent invocation.

type RunOpts

type RunOpts struct {
	Prompt string
	// Env appends invocation-scoped environment entries to the agent process.
	// Entries later in the slice override inherited values.
	Env         []string
	CWD         string
	JSONSchema  json.RawMessage      // structured output schema (optional)
	OnChunk     func(text string)    // streaming text callback (optional)
	OnLifecycle func(LifecycleEvent) // native agent lifecycle callback (optional)
	// Session, when non-nil, asks a session-capable adapter (see
	// SessionResumer) to start or resume a durable native session. Adapters
	// without session support ignore it and run cold; the caller detects the
	// fallback via an empty Result.SessionID.
	Session *SessionRef
	// SessionFallback marks this invocation as the fresh-session retry after
	// a failed resume. Instrumentation only; adapters ignore it.
	SessionFallback bool
	// Purpose labels the pipeline duty this invocation serves (review,
	// review-fix, test-evidence, ...). Instrumentation only; adapters
	// ignore it.
	Purpose string
	// SessionFallbackReason is the low-cardinality reason a failed resume forced
	// this fresh-session retry (see db.FallbackReason*). Set only when
	// SessionFallback is true. Instrumentation only; adapters ignore it.
	SessionFallbackReason string
	// Workload, when non-nil, records the bounded size of the change this
	// invocation is working over (files and net lines), so review/fix telemetry
	// can be normalized without external git archaeology. Instrumentation only;
	// adapters ignore it.
	Workload *InvocationWorkload
	// OnAttempt receives each concrete adapter attempt, including retries and
	// fallback-provider attempts, after it completes. It is instrumentation
	// only and must not change invocation behavior.
	OnAttempt func(Attempt)
	// contains filtered or unexported fields
}

RunOpts configures a single agent invocation.

type ServerPIDInfo

type ServerPIDInfo struct {
	PID            int       `json:"pid"`
	Owner          string    `json:"owner,omitempty"`
	OwnerPID       int       `json:"owner_pid,omitempty"`
	OwnerStartedAt time.Time `json:"owner_started_at,omitempty"`
	Agent          string    `json:"agent"`
	Bin            string    `json:"bin"`
	Port           int       `json:"port"`
	StartedAt      time.Time `json:"started_at"`
}

ServerPIDInfo records a managed server's identity on disk so that a freshly started daemon can reap orphaned subprocesses left behind by a crashed predecessor. The file is written after the subprocess starts and deleted after it shuts down cleanly.

type SessionProviderMatcher

type SessionProviderMatcher interface {
	SupportsSessionProvider(string) bool
}

SessionProviderMatcher reports whether an agent can resume sessions minted by a particular provider. Fallback wrappers implement it so callers do not mistake the wrapper's name for the provider that owns a session identity.

type SessionRef

type SessionRef struct {
	// ID is the adapter-native session identity to resume. Empty starts a
	// new resumable session whose identity is reported via Result.SessionID.
	ID    string
	Agent string
}

SessionRef identifies a durable adapter-native session for RunOpts.Session.

type SessionResumer

type SessionResumer interface {
	SupportsSessionResume() bool
}

SessionResumer is the optional adapter capability for durable native session resume across invocations. Decorators must forward it; callers use SupportsSessionResume so wrapping never hides the capability.

type TokenUsage

type TokenUsage struct {
	InputTokens         int
	OutputTokens        int
	CacheReadTokens     int
	CacheCreationTokens int
	// ReasoningTokens is the output tokens the model spent on hidden reasoning,
	// when the provider reports it separately. Zero when not reported.
	ReasoningTokens       int
	Reported              bool
	CacheCreationReported bool
}

TokenUsage tracks token consumption for an agent invocation.

func (*TokenUsage) Add

func (u *TokenUsage) Add(other TokenUsage)

Add accumulates another usage into this one.

func (TokenUsage) Total

func (u TokenUsage) Total() int

Total returns input + output tokens (the billing-relevant total).

type ToolCategory

type ToolCategory string

ToolCategory is a bounded bucket for a single tool sub-command. The set is fixed and low-cardinality so the histogram stays bounded and privacy-safe: we categorize a command's intent by its leading verb and never store the command text itself.

const (
	// ToolWait is a wait/poll call that produces no work: sleeping, waiting on
	// a background job, or polling a slow subprocess (e.g. codex write_stdin).
	ToolWait ToolCategory = "wait"
	// ToolTestLint runs a test suite or a linter/formatter.
	ToolTestLint ToolCategory = "test_lint"
	// ToolEdit mutates the working tree (patch/apply, file writes, moves).
	ToolEdit ToolCategory = "edit"
	// ToolRead inspects the working tree without mutating it (cat, grep, ls).
	ToolRead ToolCategory = "read"
	// ToolGit is any git invocation.
	ToolGit ToolCategory = "git"
	// ToolOther is anything not matched by the buckets above.
	ToolOther ToolCategory = "other"
)

func ClassifyToolCommand

func ClassifyToolCommand(command string) []ToolCategory

ClassifyToolCommand classifies one tool invocation's command into one bucket per chained sub-command. It unwraps a shell wrapper (`bash -lc '<script>'`), splits the script on command separators, and classifies each sub-command by its leading verb. It is a deliberately simple, allocation-light heuristic - the same leading-verb approach the efficiency audits used - and it never retains the command text.

type ToolCategoryCounts

type ToolCategoryCounts struct {
	Wait     int
	TestLint int
	Edit     int
	Read     int
	Git      int
	Other    int
}

ToolCategoryCounts is the bounded histogram of classified tool sub-commands for one invocation. Because a compound command (`go test && git commit`) contributes one count per sub-command, the sum of these fields can exceed InvocationMetrics.ToolCalls, which counts whole tool invocations.

func (*ToolCategoryCounts) Add

func (c *ToolCategoryCounts) Add(category ToolCategory)

Add increments the bucket for category.

func (ToolCategoryCounts) Total

func (c ToolCategoryCounts) Total() int

Total returns the number of classified sub-commands.

Jump to

Keyboard shortcuts

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