From 496fb085d9cc85deff20fdc7d3e09660e035fdd3 Mon Sep 17 00:00:00 2001 From: Andrew Hamon Date: Sun, 15 Mar 2026 14:17:08 -0700 Subject: [PATCH 1/3] feat(coderd): add GET /api/v2/workspaceagents/me endpoint Add a REST endpoint that allows a workspace agent to query its own metadata using its session token. This returns the same response as the existing GET /api/v2/workspaceagents/{id} endpoint but authenticates via the agent token instead of requiring a user API key. This enables scripts and services running on workspace instances to retrieve agent metadata (environment variables, workspace info, etc.) using simple HTTP calls, without needing to speak the dRPC protocol used by the agent's manifest API. Co-Authored-By: Claude Opus 4.6 --- coderd/coderd.go | 1 + coderd/workspaceagents.go | 89 ++++++++++++++++++++++++++++++++++ coderd/workspaceagents_test.go | 23 +++++++++ codersdk/agentsdk/agentsdk.go | 17 +++++++ 4 files changed, 130 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index f1e8622a4b09e..582f663b9849a 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1575,6 +1575,7 @@ func New(options *Options) *API { ).Get("/connection", api.workspaceAgentConnectionGeneric) r.Route("/me", func(r chi.Router) { r.Use(workspaceAgentInfo) + r.Get("/", api.workspaceAgentMe) r.Group(func(r chi.Router) { r.Use( // Override the request_type for agent rpc traffic. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 27719cfdea249..ebe8a892cac21 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -125,6 +125,95 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, apiAgent) } +// @Summary Get authenticated workspace agent +// @ID get-authenticated-workspace-agent +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Success 200 {object} codersdk.WorkspaceAgent +// @Router /workspaceagents/me [get] +func (api *API) workspaceAgentMe(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + agent = httpmw.WorkspaceAgent(r) + ) + + // The /me middleware only provides the agent and build, but the + // response also needs the workspace and owner username. Fetch them + // using the authenticated agent's ID. + //nolint:gocritic // The agent RBAC scope may not cover this cross-table query. + waws, err := api.Database.GetWorkspaceAgentAndWorkspaceByID(dbauthz.AsSystemRestricted(ctx), agent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent.", + Detail: err.Error(), + }) + return + } + + var ( + dbApps []database.WorkspaceApp + scripts []database.WorkspaceAgentScript + logSources []database.WorkspaceAgentLogSource + ) + + var eg errgroup.Group + eg.Go(func() (err error) { + dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, agent.ID) + return err + }) + eg.Go(func() (err error) { + //nolint:gocritic // TODO: can we make this not require system restricted? + scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{agent.ID}) + return err + }) + eg.Go(func() (err error) { + //nolint:gocritic // TODO: can we make this not require system restricted? + logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{agent.ID}) + return err + }) + err = eg.Wait() + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agent.", + Detail: err.Error(), + }) + return + } + + appIDs := []uuid.UUID{} + for _, app := range dbApps { + appIDs = append(appIDs, app.ID) + } + // nolint:gocritic // This is a system restricted operation. + statuses, err := api.Database.GetWorkspaceAppStatusesByAppIDs(dbauthz.AsSystemRestricted(ctx), appIDs) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace app statuses.", + Detail: err.Error(), + }) + return + } + + apiAgent, err := db2sdk.WorkspaceAgent( + api.DERPMap(), *api.TailnetCoordinator.Load(), waws.WorkspaceAgent, db2sdk.Apps(dbApps, statuses, waws.WorkspaceAgent, waws.OwnerUsername, waws.WorkspaceTable), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), + ) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error reading workspace agent.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, apiAgent) +} + const AgentAPIVersionREST = "1.0" // @Summary Patch workspace agent logs diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 48b00b405b9bb..b7ce6c782135f 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -92,6 +92,29 @@ func TestWorkspaceAgent(t *testing.T) { require.NoError(t, err) require.True(t, workspace.LatestBuild.Resources[0].Agents[0].Health.Healthy) }) + t.Run("Me", func(t *testing.T) { + t.Parallel() + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + tmpDir := t.TempDir() + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Directory = tmpDir + return agents + }).Do() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(r.AgentToken)) + me, err := agentClient.Me(ctx) + require.NoError(t, err) + require.Equal(t, r.Agents[0].ID, me.ID) + require.Equal(t, tmpDir, me.Directory) + }) t.Run("HasFallbackTroubleshootingURL", func(t *testing.T) { t.Parallel() client, db := coderdtest.NewWithDatabase(t, nil) diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 4255e41d49a94..ec70152384176 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -66,6 +66,23 @@ type Client struct { SDK *codersdk.Client } +// Me returns the workspace agent metadata for the agent authenticated +// by the session token. +func (c *Client) Me(ctx context.Context) (codersdk.WorkspaceAgent, error) { + res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me", nil) + if err != nil { + return codersdk.WorkspaceAgent{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return codersdk.WorkspaceAgent{}, codersdk.ReadBodyAsError(res) + } + + var agent codersdk.WorkspaceAgent + return agent, json.NewDecoder(res.Body).Decode(&agent) +} + type GitSSHKey struct { PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` From f4f3878cbd797a47cc79dd346fb446875bb433d4 Mon Sep 17 00:00:00 2001 From: Andrew Hamon Date: Sun, 15 Mar 2026 14:45:28 -0700 Subject: [PATCH 2/3] refactor(coderd): extract shared workspaceAgent response logic Extract the common logic between workspaceAgent and workspaceAgentMe into writeWorkspaceAgentResponse to reduce duplication. Co-Authored-By: Claude Opus 4.6 --- coderd/workspaceagents.go | 87 +++++++-------------------------------- 1 file changed, 16 insertions(+), 71 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index ebe8a892cac21..db00e0dae858b 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -60,69 +60,8 @@ import ( // @Success 200 {object} codersdk.WorkspaceAgent // @Router /workspaceagents/{workspaceagent} [get] func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - waws = httpmw.WorkspaceAgentAndWorkspaceParam(r) - dbApps []database.WorkspaceApp - scripts []database.WorkspaceAgentScript - logSources []database.WorkspaceAgentLogSource - ) - - var eg errgroup.Group - eg.Go(func() (err error) { - dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, waws.WorkspaceAgent.ID) - return err - }) - eg.Go(func() (err error) { - //nolint:gocritic // TODO: can we make this not require system restricted? - scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) - return err - }) - eg.Go(func() (err error) { - //nolint:gocritic // TODO: can we make this not require system restricted? - logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) - return err - }) - err := eg.Wait() - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agent.", - Detail: err.Error(), - }) - return - } - - appIDs := []uuid.UUID{} - for _, app := range dbApps { - appIDs = append(appIDs, app.ID) - } - // nolint:gocritic // This is a system restricted operation. - statuses, err := api.Database.GetWorkspaceAppStatusesByAppIDs(dbauthz.AsSystemRestricted(ctx), appIDs) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace app statuses.", - Detail: err.Error(), - }) - return - } - - apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), waws.WorkspaceAgent, db2sdk.Apps(dbApps, statuses, waws.WorkspaceAgent, waws.OwnerUsername, waws.WorkspaceTable), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, - api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), - ) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error reading workspace agent.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, apiAgent) + waws := httpmw.WorkspaceAgentAndWorkspaceParam(r) + api.writeWorkspaceAgentResponse(rw, r, waws) } // @Summary Get authenticated workspace agent @@ -133,10 +72,8 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { // @Success 200 {object} codersdk.WorkspaceAgent // @Router /workspaceagents/me [get] func (api *API) workspaceAgentMe(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - agent = httpmw.WorkspaceAgent(r) - ) + ctx := r.Context() + agent := httpmw.WorkspaceAgent(r) // The /me middleware only provides the agent and build, but the // response also needs the workspace and owner username. Fetch them @@ -151,7 +88,15 @@ func (api *API) workspaceAgentMe(rw http.ResponseWriter, r *http.Request) { return } + api.writeWorkspaceAgentResponse(rw, r, waws) +} + +// writeWorkspaceAgentResponse fetches apps, scripts, log sources, and +// statuses for the given agent and writes a codersdk.WorkspaceAgent +// JSON response. +func (api *API) writeWorkspaceAgentResponse(rw http.ResponseWriter, r *http.Request, waws database.GetWorkspaceAgentAndWorkspaceByIDRow) { var ( + ctx = r.Context() dbApps []database.WorkspaceApp scripts []database.WorkspaceAgentScript logSources []database.WorkspaceAgentLogSource @@ -159,20 +104,20 @@ func (api *API) workspaceAgentMe(rw http.ResponseWriter, r *http.Request) { var eg errgroup.Group eg.Go(func() (err error) { - dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, agent.ID) + dbApps, err = api.Database.GetWorkspaceAppsByAgentID(ctx, waws.WorkspaceAgent.ID) return err }) eg.Go(func() (err error) { //nolint:gocritic // TODO: can we make this not require system restricted? - scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{agent.ID}) + scripts, err = api.Database.GetWorkspaceAgentScriptsByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) return err }) eg.Go(func() (err error) { //nolint:gocritic // TODO: can we make this not require system restricted? - logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{agent.ID}) + logSources, err = api.Database.GetWorkspaceAgentLogSourcesByAgentIDs(dbauthz.AsSystemRestricted(ctx), []uuid.UUID{waws.WorkspaceAgent.ID}) return err }) - err = eg.Wait() + err := eg.Wait() if httpapi.Is404Error(err) { httpapi.ResourceNotFound(rw) return From 9b4d3bf348f47ff32b170bd4c40f342e9a04fba4 Mon Sep 17 00:00:00 2001 From: Andrew Hamon Date: Wed, 18 Mar 2026 04:54:31 +0000 Subject: [PATCH 3/3] update docs --- coderd/apidoc/docs.go | 39 ++++++++-- coderd/apidoc/swagger.json | 35 +++++++-- docs/reference/api/agents.md | 143 ++++++++++++++++++++++++++++++++++ docs/reference/api/schemas.md | 28 +++---- 4 files changed, 218 insertions(+), 27 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d441b185b135d..6e2f6c63eb7eb 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9613,6 +9613,31 @@ const docTemplate = `{ } } }, + "/workspaceagents/me": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Get authenticated workspace agent", + "operationId": "get-authenticated-workspace-agent", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgent" + } + } + } + } + }, "/workspaceagents/me/app-status": { "patch": { "security": [ @@ -23148,19 +23173,19 @@ const docTemplate = `{ "type": "object", "properties": { "forceQuery": { - "description": "append a query ('?') even if RawQuery is empty", + "description": "ForceQuery indicates whether the original URL contained a query ('?') character.\nWhen set, the String method will include a trailing '?', even when RawQuery is empty.", "type": "boolean" }, "fragment": { - "description": "fragment for references, without '#'", + "description": "fragment for references (without '#')", "type": "string" }, "host": { - "description": "host or host:port (see Hostname and Port methods)", + "description": "\"host\" or \"host:port\" (see Hostname and Port methods)", "type": "string" }, "omitHost": { - "description": "do not emit empty host (authority)", + "description": "OmitHost indicates the URL has an empty host (authority).\nWhen set, the String method will not include the host when it is empty.", "type": "boolean" }, "opaque": { @@ -23172,15 +23197,15 @@ const docTemplate = `{ "type": "string" }, "rawFragment": { - "description": "encoded fragment hint (see EscapedFragment method)", + "description": "RawFragment is an optional field containing an encoded fragment hint.\nSee the EscapedFragment method for more details.\n\nIn general, code should call EscapedFragment instead of reading RawFragment.", "type": "string" }, "rawPath": { - "description": "encoded path hint (see EscapedPath method)", + "description": "RawPath is an optional field containing an encoded path hint.\nSee the EscapedPath method for more details.\n\nIn general, code should call EscapedPath instead of reading RawPath.", "type": "string" }, "rawQuery": { - "description": "encoded query values, without '?'", + "description": "RawQuery contains the encoded query values, without the initial '?'.\nUse URL.Query to decode the query.", "type": "string" }, "scheme": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5bc51ca337091..fe1fe79b073dd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8508,6 +8508,27 @@ } } }, + "/workspaceagents/me": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Get authenticated workspace agent", + "operationId": "get-authenticated-workspace-agent", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgent" + } + } + } + } + }, "/workspaceagents/me/app-status": { "patch": { "security": [ @@ -21297,19 +21318,19 @@ "type": "object", "properties": { "forceQuery": { - "description": "append a query ('?') even if RawQuery is empty", + "description": "ForceQuery indicates whether the original URL contained a query ('?') character.\nWhen set, the String method will include a trailing '?', even when RawQuery is empty.", "type": "boolean" }, "fragment": { - "description": "fragment for references, without '#'", + "description": "fragment for references (without '#')", "type": "string" }, "host": { - "description": "host or host:port (see Hostname and Port methods)", + "description": "\"host\" or \"host:port\" (see Hostname and Port methods)", "type": "string" }, "omitHost": { - "description": "do not emit empty host (authority)", + "description": "OmitHost indicates the URL has an empty host (authority).\nWhen set, the String method will not include the host when it is empty.", "type": "boolean" }, "opaque": { @@ -21321,15 +21342,15 @@ "type": "string" }, "rawFragment": { - "description": "encoded fragment hint (see EscapedFragment method)", + "description": "RawFragment is an optional field containing an encoded fragment hint.\nSee the EscapedFragment method for more details.\n\nIn general, code should call EscapedFragment instead of reading RawFragment.", "type": "string" }, "rawPath": { - "description": "encoded path hint (see EscapedPath method)", + "description": "RawPath is an optional field containing an encoded path hint.\nSee the EscapedPath method for more details.\n\nIn general, code should call EscapedPath instead of reading RawPath.", "type": "string" }, "rawQuery": { - "description": "encoded query values, without '?'", + "description": "RawQuery contains the encoded query values, without the initial '?'.\nUse URL.Query to decode the query.", "type": "string" }, "scheme": { diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 8252582093f9b..88a48d145dba7 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -180,6 +180,149 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get authenticated workspace agent + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/workspaceagents/me \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /workspaceagents/me` + +### Example responses + +> 200 Response + +```json +{ + "api_version": "string", + "apps": [ + { + "command": "string", + "display_name": "string", + "external": true, + "group": "string", + "health": "disabled", + "healthcheck": { + "interval": 0, + "threshold": 0, + "url": "string" + }, + "hidden": true, + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "open_in": "slim-window", + "sharing_level": "owner", + "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], + "subdomain": true, + "subdomain_name": "string", + "tooltip": "string", + "url": "string" + } + ], + "architecture": "string", + "connection_timeout_seconds": 0, + "created_at": "2019-08-24T14:15:22Z", + "directory": "string", + "disconnected_at": "2019-08-24T14:15:22Z", + "display_apps": [ + "vscode" + ], + "environment_variables": { + "property1": "string", + "property2": "string" + }, + "expanded_directory": "string", + "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": false, + "reason": "agent has lost connection" + }, + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "instance_id": "string", + "last_connected_at": "2019-08-24T14:15:22Z", + "latency": { + "property1": { + "latency_ms": 0, + "preferred": true + }, + "property2": { + "latency_ms": 0, + "preferred": true + } + }, + "lifecycle_state": "created", + "log_sources": [ + { + "created_at": "2019-08-24T14:15:22Z", + "display_name": "string", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "workspace_agent_id": "7ad2e618-fea7-4c1a-b70a-f501566a72f1" + } + ], + "logs_length": 0, + "logs_overflowed": true, + "name": "string", + "operating_system": "string", + "parent_id": { + "uuid": "string", + "valid": true + }, + "ready_at": "2019-08-24T14:15:22Z", + "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", + "scripts": [ + { + "cron": "string", + "display_name": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "log_path": "string", + "log_source_id": "4197ab25-95cf-4b91-9c78-f7f2af5d353a", + "run_on_start": true, + "run_on_stop": true, + "script": "string", + "start_blocks_login": true, + "timeout": 0 + } + ], + "started_at": "2019-08-24T14:15:22Z", + "startup_script_behavior": "blocking", + "status": "connecting", + "subsystems": [ + "envbox" + ], + "troubleshooting_url": "string", + "updated_at": "2019-08-24T14:15:22Z", + "version": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgent](schemas.md#codersdkworkspaceagent) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Patch workspace agent app status ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 3a197078bfa79..d61eb5538bad8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -14315,19 +14315,21 @@ None ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------|------------------------------|----------|--------------|----------------------------------------------------| -| `forceQuery` | boolean | false | | append a query ('?') even if RawQuery is empty | -| `fragment` | string | false | | fragment for references, without '#' | -| `host` | string | false | | host or host:port (see Hostname and Port methods) | -| `omitHost` | boolean | false | | do not emit empty host (authority) | -| `opaque` | string | false | | encoded opaque data | -| `path` | string | false | | path (relative paths may omit leading slash) | -| `rawFragment` | string | false | | encoded fragment hint (see EscapedFragment method) | -| `rawPath` | string | false | | encoded path hint (see EscapedPath method) | -| `rawQuery` | string | false | | encoded query values, without '?' | -| `scheme` | string | false | | | -| `user` | [url.Userinfo](#urluserinfo) | false | | username and password information | +| Name | Type | Required | Restrictions | Description | +|--------------|---------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `forceQuery` | boolean | false | | Forcequery indicates whether the original URL contained a query ('?') character. When set, the String method will include a trailing '?', even when RawQuery is empty. | +| `fragment` | string | false | | fragment for references (without '#') | +| `host` | string | false | | "host" or "host:port" (see Hostname and Port methods) | +| `omitHost` | boolean | false | | Omithost indicates the URL has an empty host (authority). When set, the String method will not include the host when it is empty. | +| `opaque` | string | false | | encoded opaque data | +| `path` | string | false | | path (relative paths may omit leading slash) | +|`rawFragment`|string|false||Rawfragment is an optional field containing an encoded fragment hint. See the EscapedFragment method for more details. +In general, code should call EscapedFragment instead of reading RawFragment.| +|`rawPath`|string|false||Rawpath is an optional field containing an encoded path hint. See the EscapedPath method for more details. +In general, code should call EscapedPath instead of reading RawPath.| +|`rawQuery`|string|false||Rawquery contains the encoded query values, without the initial '?'. Use URL.Query to decode the query.| +|`scheme`|string|false||| +|`user`|[url.Userinfo](#urluserinfo)|false||username and password information| ## serpent.ValueSource