You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/10/08 01:59:56 UTC

(skywalking-ai-sessionizer) branch main updated: Let view.hide decide, and take a request's hide only when view.hide_override is on (#54)

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git


The following commit(s) were added to refs/heads/main by this push:
     new b5c2c30  Let view.hide decide, and take a request's hide only when view.hide_override is on (#54)
b5c2c30 is described below

commit b5c2c30c144270da0f2c4e28be57d513bdd48bb2
Author: 吴晟 Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Oct 8 09:59:50 2026 +0800

    Let view.hide decide, and take a request's hide only when view.hide_override is on (#54)
    
    view.hide is now the whole answer by default. A request that carries the
    hide parameter is refused with status 400, since serving it anything else
    would not be what it asked for. Before this, the parameter was always taken
    and added names to view.hide.
    
    view.hide_override, off by default, lets a request's hide replace view.hide
    for that request. A host that serves the API through its own route then
    decides what each reader is kept from. A request that sets no hide is served
    what view.hide says, and hide= with no name withholds nothing.
    
    asz view and asz server say at start-up when the override is on. The
    configuration, asz.view and command-line pages and the 0.6.0 changes say the
    same.
---
 asz.yaml                                 | 15 ++++++---
 cmd/asz/view.go                          | 20 ++++++++----
 docs/en/changes/changes.md               | 13 +++++---
 docs/en/formats/asz-view.md              | 31 ++++++++++--------
 docs/en/guides/command-line-internals.md | 15 +++++----
 docs/en/setup/command-line.md            |  4 +--
 docs/en/setup/configuration.md           |  9 ++---
 internal/config/config.go                | 14 +++++---
 internal/config/config_test.go           |  8 ++++-
 internal/view/api.go                     |  4 +--
 internal/view/index.html                 |  7 ++--
 internal/view/page.html                  |  8 +++--
 internal/view/view.go                    |  7 ++--
 internal/view/withhold.go                | 51 +++++++++++++++++++----------
 internal/view/withhold_test.go           | 56 ++++++++++++++++++++++++++------
 tests/langchain_withhold_test.go         | 12 +++++--
 16 files changed, 185 insertions(+), 89 deletions(-)

diff --git a/asz.yaml b/asz.yaml
index 4a1537b..f3228a5 100644
--- a/asz.yaml
+++ b/asz.yaml
@@ -285,16 +285,21 @@ export:
     metrics: true
 
 # What asz view and asz server serve. asz knows nothing about who is
-# reading: these settings apply to every reader of this instance, and two
+# reading: these settings apply to every reader of this instance. Two
 # audiences are two instances over the same root, each behind the
-# deployment's own authentication.
+# deployment's own authentication, or one instance with hide_override on
+# behind a host that sets hide for each reader.
 view:
   # The flags whose steps the page withholds from every reader:
   # system_prompt, the system prompt a runtime sent, and tool_schemas, the
   # schemas of the tools it advertised. A withheld step keeps its place,
   # its flags and its size, loses its text, and says so with the state
   # omitted. The provider bodies go with it, since a request carries both
-  # again and a body is served whole or not at all. A host that serves the
-  # API through its own route withholds more for one reader with the hide
-  # parameter, never less. Empty hides nothing.
+  # again and a body is served whole or not at all. Empty hides nothing.
   hide: []
+  # Whether a request's hide parameter replaces hide above for that
+  # request. On, a host that serves the API through its own route decides
+  # what each reader is kept from, and hide above applies to a request that
+  # sets no hide. Off, hide above is the whole answer, and a request that
+  # carries the parameter is refused.
+  hide_override: false
diff --git a/cmd/asz/view.go b/cmd/asz/view.go
index ffeaa54..aaec3ab 100644
--- a/cmd/asz/view.go
+++ b/cmd/asz/view.go
@@ -49,7 +49,7 @@ func cmdView(cfg *config.Config, _ []config.Adapter, _ bool) error {
 	}
 	zone := storage.NewZone(zoneRoot)
 	srv := view.NewWithGlossaries(zone, glossaries())
-	if err := srv.SetHide(cfg.View.Hide); err != nil {
+	if err := srv.SetHide(cfg.View.Hide, cfg.View.HideOverride); err != nil {
 		return err
 	}
 	ids, err := srv.List()
@@ -149,7 +149,7 @@ func cmdServer(cfg *config.Config, ads []config.Adapter, once bool) error {
 	}
 	zone := storage.NewZone(zoneRoot)
 	srv := view.NewWithGlossaries(zone, glossaries())
-	if err := srv.SetHide(cfg.View.Hide); err != nil {
+	if err := srv.SetHide(cfg.View.Hide, cfg.View.HideOverride); err != nil {
 		return err
 	}
 
@@ -203,18 +203,24 @@ func cmdServer(cfg *config.Config, ads []config.Adapter, once bool) error {
 	return http.Serve(ln, srv.Handler())
 }
 
-// printWithholding says what the page withholds from every reader.
+// printWithholding says what the page withholds, and whether a request may
+// say otherwise.
 //
 // A record is named for what it carries when it lands, and a root landed
 // before asz named these carries no such name. Withholding there finds
 // nothing, and the only sign is a zero in each document's summary, so the
 // operator is told here, once, before anyone reads.
 func printWithholding(cfg *config.Config) {
-	if len(cfg.View.Hide) == 0 {
-		return
+	const landedBefore = " A record landed before asz named these carries no name and is shown. Collect such a root again into a new one to withhold there."
+	names := strings.Join(cfg.View.Hide, " and ")
+	switch {
+	case cfg.View.HideOverride && names == "":
+		fmt.Println("withhold : nothing, unless a request's hide parameter names what to withhold (view.hide_override is on)." + landedBefore)
+	case cfg.View.HideOverride:
+		fmt.Printf("withhold : %s, unless a request's hide parameter names another set (view.hide_override is on).%s\n", names, landedBefore)
+	case names != "":
+		fmt.Printf("withhold : %s, from every reader.%s\n", names, landedBefore)
 	}
-	fmt.Printf("withhold : %s, from every reader. A record landed before asz named these carries no name and is shown. Collect such a root again into a new one to withhold there.\n",
-		strings.Join(cfg.View.Hide, " and "))
 }
 
 // serveAddr listens on the address the command was given, or the default.
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 2f0de21..8f88059 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -105,10 +105,12 @@
   to the people an agent served may need to keep from them what the runtime sent the model, while
   the operator still sees it. asz knows nothing about who is reading. It withholds by the flags the
   adapter set, never by the text or the size of a part. `view.hide` in the configuration lists the
-  flags that `asz view` and `asz server` withhold from every reader. A `hide` parameter on the
-  document, record and files endpoints adds names for one request and never takes one away. A host
-  that serves the API through its own route adds it to every request, and so decides per reader. The
-  page passes a `hide` in its own address on to its document, record and files calls.
+  flags that `asz view` and `asz server` withhold, and by default it is the whole answer: a request
+  that carries a `hide` parameter is refused with status 400. With `view.hide_override` on, a `hide`
+  parameter on the document, record and files endpoints replaces `view.hide` for that request, and
+  `hide=` with no name withholds nothing. A host that serves the API through its own route sets it
+  on every request, and so decides per reader. The page passes a `hide` in its own address on to its
+  document, record and files calls.
 - A withheld step keeps its node, its flags and its size, loses its text and says `omitted`.
   `summary.withheld` counts what was withheld, and lists every name asked for, a zero included. So
   a filter over a root landed before the flags shows that it found nothing. For a reader that
@@ -128,7 +130,8 @@
 
 - The `view` section is read strictly. A key it does not have is refused, where every other section
   ignores one, because a misspelled key there would show everything to everyone. `hide` is read only
-  inside the `view` section, and written anywhere else it is ignored and withholds nothing. Only the
+  inside the `view` section, and written anywhere else it is ignored and withholds nothing.
+  `view.hide_override`, off by default, lets a request's `hide` parameter replace `view.hide`. Only the
   first YAML document of a file is read, so a file with a later one that holds anything is refused
   rather than read in part. The rule is on the [configuration page](../setup/configuration.md#view).
 
diff --git a/docs/en/formats/asz-view.md b/docs/en/formats/asz-view.md
index be896be..70cce55 100644
--- a/docs/en/formats/asz-view.md
+++ b/docs/en/formats/asz-view.md
@@ -261,20 +261,23 @@ An operator who serves a conversation to the people an agent served may need to
 the runtime sent the model: the system prompt and the tool schemas. The record that carries each is
 named by its adapter with a flag, `system_prompt` or `tool_schemas`, as [Session
 Data](session-data.md#flags) lists, and a server withholds by those names, never by the text or the
-size of a part. asz knows nothing about who is reading. `asz view` withholds what its
-[configuration](../setup/configuration.md#view) says for every reader, and a `hide` parameter on
-`/api/c/{id}/view`, `/api/c/{id}/record/{seq}/{row}` and `/api/c/{id}/files` adds names for one
-request and never takes one away: `?hide=system_prompt,tool_schemas`, or the parameter given more
-than once. Spaces around a name and empty entries are ignored, and a repeated name counts once. A
-name that is not a flag a reader may withhold is refused with status 400. So is any
-other key whose letters alone spell `hide`, in any case, such as `Hide`, `hide[]` or `hide[0]`, and
-a query that does not parse, since either would otherwise read as no `hide` at all. A host that
-serves the API through its own route decides per reader, and adds the parameter to every request it
-serves for a reader who may not see the material: the document, every record and every file. asz's
-own page passes a `hide` in its address on to all three, so the Evidence tab never shows what the
-document withholds. Its links between the list and the conversations keep it too. That is not access
-control, because a reader can edit the address. Two audiences are two instances over the same root,
-each behind the deployment's own authentication.
+size of a part. asz knows nothing about who is reading. `asz view` withholds what `view.hide` in
+its [configuration](../setup/configuration.md#view) says, and by default that is the whole answer: a
+request that carries a `hide` parameter is refused with status 400. With `view.hide_override` on, a
+`hide` parameter on `/api/c/{id}/view`, `/api/c/{id}/record/{seq}/{row}` and `/api/c/{id}/files`
+replaces `view.hide` for that request: `?hide=system_prompt,tool_schemas`, or the parameter given
+more than once, and `?hide=` with no name withholds nothing. A request that sets no `hide` is served
+what `view.hide` says. Spaces around a name and empty entries are ignored, and a repeated name counts
+once. A name that is not a flag a reader may withhold is refused with status 400. So is any other key
+whose letters alone spell `hide`, in any case, such as `Hide`, `hide[]` or `hide[0]`, and a query
+that does not parse, since either would otherwise read as no `hide` at all. A host that serves the API
+through its own route, on an instance with `view.hide_override` on, decides per reader, and sets the
+parameter on every request it serves: the document, every record and every file. asz's own page
+passes a `hide` in its address on to all three, so the Evidence tab never shows what the document
+withholds, and shows the refusal of an instance that does not take the parameter. Its links between
+the list and the conversations keep it too. That is not access control, because a reader can edit
+the address. Two audiences are two instances over the same root, each behind the deployment's own
+authentication, or one such host.
 
 Which records carry the flags depends on the adapter. The Claude Code adapter names two
 attachments: the prompt snapshot that Claude Code and a runtime built on the Agent SDK write, and
diff --git a/docs/en/guides/command-line-internals.md b/docs/en/guides/command-line-internals.md
index 3024c09..dba1771 100644
--- a/docs/en/guides/command-line-internals.md
+++ b/docs/en/guides/command-line-internals.md
@@ -234,13 +234,14 @@ counting from 1. The record is read from disk on every request and never cached,
 wanted only when someone opens it. The answer is 404 when the conversation, the sequence or the row
 does not exist.
 
-The three `/api/c/{id}` paths take `hide`, the flags this reader withholds beyond what the instance
-withholds for everyone, as [Withholding](../formats/asz-view.md#withholding) says. A record that
-carries one keeps its envelope and loses its content. A reader that withholds anything is served no
-provider body: the files endpoint and the record endpoint, for a record of a body file, answer 403.
-The answer is 400 for a name that cannot be withheld, for `hide` spelled another way, for a query
-that does not parse, and from the files endpoint for a file that is not a provider body or whose
-header does not read.
+The three `/api/c/{id}` paths withhold what `view.hide` names. With `view.hide_override` on they
+take `hide`, the flags this request withholds in place of `view.hide`, as
+[Withholding](../formats/asz-view.md#withholding) says. A record that carries one keeps its envelope
+and loses its content. A reader that withholds anything is served no provider body: the files
+endpoint and the record endpoint, for a record of a body file, answer 403. The answer is 400 for
+`hide` on an instance with `view.hide_override` off, for a name that cannot be withheld, for `hide`
+spelled another way, for a query that does not parse, and from the files endpoint for a file that is
+not a provider body or whose header does not read.
 
 `/api/glossary` takes an optional `dialect`, and without one answers in the vocabulary the root's
 own landed files were read in — a root of LangChain conversations is not described in Claude Code's
diff --git a/docs/en/setup/command-line.md b/docs/en/setup/command-line.md
index 35aac65..2b9bbda 100644
--- a/docs/en/setup/command-line.md
+++ b/docs/en/setup/command-line.md
@@ -57,8 +57,8 @@ data collected elsewhere or by another process. The list is at `/`, and one conv
 `/c/{id}`. The Evidence tab shows the collected record behind a step, and the Prompt tab a model
 call's request and response, when [provider bodies](claude-code-provider-bodies.md) were collected.
 The page withholds from every reader what [`view.hide`](configuration.md#view) names, the system
-prompt and the tool schemas a runtime sent. `server` does the same. `conversation` withholds
-nothing. See [Withholding](../formats/asz-view.md#withholding).
+prompt and the tool schemas a runtime sent, unless `view.hide_override` lets a request's `hide`
+parameter name another set. `server` does the same. `conversation` withholds nothing. See [Withholding](../formats/asz-view.md#withholding).
 
 ## push
 
diff --git a/docs/en/setup/configuration.md b/docs/en/setup/configuration.md
index 5e28c9d..b43e628 100644
--- a/docs/en/setup/configuration.md
+++ b/docs/en/setup/configuration.md
@@ -137,8 +137,8 @@ export:
 
 What `asz view` and `asz server` serve. asz knows nothing about who is reading, so these apply to
 every reader of the instance. Two audiences are two instances over the same root, each behind the
-deployment's own authentication. Or one host serves the API through its own route, and adds `hide`
-to every request for a reader who may not see it. See
+deployment's own authentication. Or one instance with `hide_override` on serves the API through a
+host's own route, and the host sets the `hide` parameter on every request for each reader. See
 [Withholding](../formats/asz-view.md#withholding).
 
 A key this section does not have is refused, and the refusal names the key and its line. Every
@@ -152,13 +152,14 @@ configuration does not have: ignored, and withholding nothing.
 Only the first YAML document of a file is read, so a file with a later document that holds anything
 is refused. `view` is read from 0.6.0. An older asz ignores it, and withholds nothing.
 
-When `hide` is set, `asz view` and `asz server` say so as they start. A record is named for what it
+When `hide` is set, or `hide_override` is on, `asz view` and `asz server` say so as they start. A record is named for what it
 carries when it lands, so a root collected before asz named these records carries no names, and
 withholds nothing. Collect it again into a new root to withhold there.
 
 | Key | Default | Meaning |
 | --- | --- | --- |
-| `hide` | `[]` | The flags whose steps the page withholds from every reader: `system_prompt`, `tool_schemas`. A withheld step keeps its place, its flags and its size, loses its text, and has state `omitted`. The provider bodies go with it, because a request carries both again. |
+| `hide` | `[]` | The flags whose steps the page withholds from every reader: `system_prompt`, `tool_schemas`. A withheld step keeps its place, its flags and its size, loses its text, and has state `omitted`. The provider bodies go with it, because a request carries both again. With `hide_override` on, this applies to a request that sets no `hide` parameter. |
+| `hide_override` | `false` | Whether a request's `hide` parameter replaces `hide` for that request. On, a host that serves the API through its own route decides what each reader is kept from, and `hide=` with no name withholds nothing. Off, `hide` is the whole answer, and a request that carries the parameter is refused with status 400. |
 
 ## The changes adapter
 
diff --git a/internal/config/config.go b/internal/config/config.go
index 90a74e6..6fa6f6a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -53,12 +53,17 @@ type View struct {
 	// keeps its place, its flags and its size, loses its text, and says so
 	// with the state omitted. The provider bodies go with it, since a
 	// request carries both again and a body is served whole or not at all.
-	// A host that serves the API through its own route withholds more for
-	// one reader with the hide parameter, never less. asz knows nothing
-	// about who is reading: one instance per audience, each behind the
-	// deployment's own authentication, is how two audiences are served.
+	// asz knows nothing about who is reading: one instance per audience,
+	// each behind the deployment's own authentication, is how two audiences
+	// are served, unless HideOverride lets a host decide per reader.
 	// Empty hides nothing.
 	Hide []string `yaml:"hide"`
+	// HideOverride lets a request's hide parameter replace Hide for that
+	// request, so a host that serves the API through its own route decides
+	// what each reader is kept from. It is off by default: Hide is then the
+	// whole answer, and a request that carries the parameter is refused,
+	// since serving it anything else would not be what it asked for.
+	HideOverride bool `yaml:"hide_override"`
 }
 
 // Metrics configures what asz derives from the landed files. It is one
@@ -489,6 +494,7 @@ func Load(path string) (*Config, error) {
 		// hide is a set, and a name written twice is withheld once.
 		cfg.View.Hide = slices.Compact(slices.Sorted(slices.Values(loaded.View.Hide)))
 	}
+	cfg.View.HideOverride = loaded.View.HideOverride
 	return cfg, cfg.Validate()
 }
 
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 0ec212b..5d0393a 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -356,6 +356,8 @@ func TestAMistakenViewKeyIsRefused(t *testing.T) {
 		{"a second document", "storage:\n  root: ./data\n---\nview:\n  hide: [system_prompt]\n", false},
 		{"a first document left empty", "---\n---\nview:\n  hide: [system_prompt]\n", false},
 		{"a document marker at the end", "view:\n  hide: [system_prompt]\n---\n", true},
+		{"hide_override in the view section", "view:\n  hide: [system_prompt]\n  hide_override: true\n", true},
+		{"a misspelled hide_override", "view:\n  hide_overide: true\n", false},
 	} {
 		path := filepath.Join(t.TempDir(), "asz.yaml")
 		if err := os.WriteFile(path, []byte(tc.body), 0o600); err != nil {
@@ -366,10 +368,14 @@ func TestAMistakenViewKeyIsRefused(t *testing.T) {
 			t.Errorf("%s: error %v, want ok %v", tc.name, err, tc.ok)
 			continue
 		}
-		// Every file that loads with a hide withholds what it says.
+		// Every file that loads with a hide withholds what it says, and lets
+		// a request replace it only when it says so.
 		if err == nil && strings.Contains(tc.body, "system_prompt") && !slices.Contains(cfg.View.Hide, "system_prompt") {
 			t.Errorf("%s: loaded with hide %v, want system_prompt withheld", tc.name, cfg.View.Hide)
 		}
+		if err == nil && cfg.View.HideOverride != strings.Contains(tc.body, "hide_override: true") {
+			t.Errorf("%s: loaded with hide_override %v", tc.name, cfg.View.HideOverride)
+		}
 	}
 }
 
diff --git a/internal/view/api.go b/internal/view/api.go
index 47421e4..7161c87 100644
--- a/internal/view/api.go
+++ b/internal/view/api.go
@@ -275,8 +275,8 @@ var recordPath = regexp.MustCompile(`^/api/c/([^/]+)/record/(\d+)/(\d+)$`)
 // asks for a record and the Prompt tab for the files a call's bodies are cut
 // across.
 //
-// Each takes the hide parameter: what this reader withholds beyond what the
-// instance withholds for everyone. See withhold.go.
+// Each takes the hide parameter, when the instance lets a request replace
+// what it withholds. See withhold.go.
 func (s *Server) apiConversation(w http.ResponseWriter, r *http.Request) {
 	// A query that does not parse may hold a hide that cannot be read, so
 	// it is refused rather than read without it.
diff --git a/internal/view/index.html b/internal/view/index.html
index a893539..f4d3473 100644
--- a/internal/view/index.html
+++ b/internal/view/index.html
@@ -180,9 +180,10 @@ tbody tr:hover td { background: var(--sw-accent-soft); }
 <script>
 "use strict";
 // A hide in the list's address goes with the reader into each conversation
-// and back, so every page they move between withholds the same. It is not
-// access control: a host that withholds per reader adds hide to every request.
-// A key spelled another way goes too, so the server refuses it.
+// and back, so every page they move between withholds the same. The server
+// takes it only when view.hide_override is on, and refuses it otherwise. It is
+// not access control: a host that withholds per reader sets hide on every
+// request. A key spelled another way goes too, so the server refuses it.
 // A `;` in the address is not a separator the API accepts, and a hide written
 // after one would be read as part of another value and dropped here, so the page
 // would ask for the whole conversation. The API refuses such a query, and so does
diff --git a/internal/view/page.html b/internal/view/page.html
index b840021..af5e9b7 100644
--- a/internal/view/page.html
+++ b/internal/view/page.html
@@ -107,9 +107,11 @@ fetch("/assets/conversation-view/host-shell/themes.json").then(r => r.json()).th
 
 // ---- what the address asks to withhold, passed to every call ----
 // The document, a record and the files are asked with the same hide, so the
-// Evidence tab never shows what the document withholds. This is not access
-// control: a reader can edit the address. A host that withholds per reader
-// adds hide to every API request it serves. Every key whose letters alone
+// Evidence tab never shows what the document withholds. The server takes hide
+// only when view.hide_override is on, and its refusal otherwise is shown as
+// the page's error. This is not access control: a reader can edit the
+// address. A host that withholds per reader sets hide on every API request it
+// serves. Every key whose letters alone
 // spell hide, in any case, is passed on as it is spelled, the rule the server
 // reads keys by, so a misspelled one reaches the server, which refuses it,
 // rather than being dropped here and withholding nothing.
diff --git a/internal/view/view.go b/internal/view/view.go
index 1904416..3e74215 100644
--- a/internal/view/view.go
+++ b/internal/view/view.go
@@ -63,9 +63,10 @@ type Server struct {
 	// so the side that reads never depends on the side that collects.
 	glossary *model.Glossary
 
-	// hide is what this instance withholds from every reader, by flag. See
-	// withhold.go.
-	hide []string
+	// hide is what this instance withholds, by flag, and hideOverride
+	// whether a request's hide parameter replaces it. See withhold.go.
+	hide         []string
+	hideOverride bool
 }
 
 // New returns a server over a zone. Nothing refreshes it until a caller says
diff --git a/internal/view/withhold.go b/internal/view/withhold.go
index 3325ec3..da95c17 100644
--- a/internal/view/withhold.go
+++ b/internal/view/withhold.go
@@ -18,6 +18,7 @@
 package view
 
 import (
+	"errors"
 	"fmt"
 	"net/url"
 	"slices"
@@ -35,11 +36,12 @@ import (
 // asz knows nothing about who is reading. What it knows is what a record
 // carries, by the flags its adapter set, and it withholds by those names,
 // never by the text or the size of a part. An instance withholds what its
-// configuration says for every reader. A request adds to that with its hide
-// parameter and never takes from it, so a host that serves the API through
-// its own route can withhold more for one reader. Two audiences are two
-// instances over the same root, each behind the deployment's own
-// authentication.
+// configuration says, view.hide, and by default that is the whole answer: a
+// request that carries the hide parameter is refused. With view.hide_override
+// on, a request's hide replaces view.hide for that request, so a host that
+// serves the API through its own route decides what each reader is kept from.
+// Otherwise two audiences are two instances over the same root, each behind
+// the deployment's own authentication.
 //
 // A withheld step keeps its node, its flags and its size, loses its text,
 // and says so with the state omitted. It is never deleted, so every count,
@@ -48,31 +50,46 @@ import (
 // and the renderer checks a body's digest, so a body is served whole or not
 // at all.
 
-// SetHide says what this instance withholds from every reader. It is set
-// before the handler serves and read by every request after.
-func (s *Server) SetHide(names []string) error {
+// SetHide says what this instance withholds, and whether a request's hide
+// parameter replaces that for the request. It is set before the handler
+// serves and read by every request after.
+func (s *Server) SetHide(names []string, override bool) error {
 	if err := sessiondata.CheckWithholdable(names); err != nil {
 		return fmt.Errorf("view: hide: %w", err)
 	}
 	s.hide = withheldNames(names)
+	s.hideOverride = override
 	return nil
 }
 
-// hideFor is what one request withholds: what the instance withholds, and
-// what the request's hide parameter adds, given once or more, each a name or
-// a comma-separated list. A name that is not a flag a reader may withhold is
-// refused rather than ignored, since withholding by it would withhold nothing
-// and the reader would not be told. So is the parameter spelled another way,
-// such as Hide, hide[] or hide[0], the forms query libraries write a list
-// in, which would otherwise be read as no parameter at all.
+// hideFor is what one request withholds. A request with no hide parameter
+// withholds what the instance withholds. One with the parameter, given once
+// or more, each a name or a comma-separated list, withholds those names
+// alone when the instance lets a request replace its own, and `hide=` with
+// no name withholds nothing. An instance that does not refuses the request,
+// since serving it anything else would not be what it asked for.
+//
+// A name that is not a flag a reader may withhold is refused rather than
+// ignored, since withholding by it would withhold nothing and the reader
+// would not be told. So is the parameter spelled another way, such as Hide,
+// hide[] or hide[0], the forms query libraries write a list in, which would
+// otherwise be read as no parameter at all.
 func (s *Server) hideFor(q url.Values) ([]string, error) {
 	for key := range q {
 		if key != "hide" && spelledAsHide(key) {
 			return nil, fmt.Errorf("view: %q is not a parameter. The parameter is hide", key)
 		}
 	}
-	names := append([]string(nil), s.hide...)
-	for _, value := range q["hide"] {
+	values, given := q["hide"]
+	if !given {
+		return withheldNames(s.hide), nil
+	}
+	if !s.hideOverride {
+		return nil, errors.New("view: this instance does not take the hide parameter. " +
+			"view.hide in its configuration decides what it withholds, and view.hide_override lets a request replace it")
+	}
+	var names []string
+	for _, value := range values {
 		for _, name := range strings.Split(value, ",") {
 			if name = strings.TrimSpace(name); name != "" {
 				names = append(names, name)
diff --git a/internal/view/withhold_test.go b/internal/view/withhold_test.go
index 8b9875f..6d6a63e 100644
--- a/internal/view/withhold_test.go
+++ b/internal/view/withhold_test.go
@@ -20,10 +20,13 @@ package view_test
 import (
 	"encoding/json"
 	"fmt"
+	"maps"
 	"net/http"
 	"net/http/httptest"
 	"os"
 	"path/filepath"
+	"slices"
+	"strings"
 	"sync"
 	"testing"
 
@@ -36,8 +39,8 @@ import (
 	"github.com/apache/skywalking-ai-sessionizer/pkg/sessionview"
 )
 
-// A view withholds by the flags the adapter set: for every reader by its
-// configuration, and for one reader by the hide parameter, which only adds.
+// A view withholds by the flags the adapter set: by its configuration, and,
+// when the configuration lets a request replace that, by the hide parameter.
 // The scenario prompt-snapshot-withheld checks the document's rules in both
 // formats. This is the HTTP layer over it: the parameter, what the record
 // endpoint answers, and what the files endpoint serves.
@@ -106,6 +109,18 @@ func TestAViewWithholdsByFlag(t *testing.T) {
 		t.Fatalf("the snapshot step is not shown whole: %+v", shown)
 	}
 
+	// By default the configuration is the whole answer, and a request that
+	// carries hide is refused, whatever it names, rather than served
+	// something other than what it asked for.
+	for _, query := range []string{"hide=system_prompt", "hide=", "hide=system_prompt&hide=tool_schemas"} {
+		if rec := get(api + "/view?" + query); rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "view.hide_override") {
+			t.Fatalf("%s on an instance that does not take hide: %d %.200s", query, rec.Code, rec.Body.String())
+		}
+	}
+	if err := srv.SetHide(nil, true); err != nil {
+		t.Fatal(err)
+	}
+
 	// One reader withholds the prompt: the step stays, its text goes, and
 	// the bodies go with it.
 	part := document(api + "/view?hide=system_prompt")
@@ -208,18 +223,34 @@ func TestAViewWithholdsByFlag(t *testing.T) {
 		t.Fatalf("a provider body record was served to a reader that withholds: %d %.200s", r.Code, r.Body.String())
 	}
 
-	// The instance withholds for every reader, and a request adds to that
-	// and never takes from it.
-	if err := srv.SetHide([]string{sessiondata.FlagToolSchemas}); err != nil {
+	// The instance's own setting applies to a request that sets no hide.
+	// When the instance takes the parameter, a request's hide replaces that
+	// setting for the request, and `hide=` with no name withholds nothing.
+	if err := srv.SetHide([]string{sessiondata.FlagToolSchemas}, false); err != nil {
 		t.Fatal(err)
 	}
-	if doc := document(api + "/view"); doc.Summary.Withheld["tool_schemas"] != schemas || doc.Summary.Withheld["system_prompt"] != 0 {
+	if doc := document(api + "/view"); fmt.Sprint(keys(doc.Summary.Withheld)) != "[provider_bodies tool_schemas]" || doc.Summary.Withheld["tool_schemas"] != schemas {
 		t.Fatalf("the instance's own setting: %v", doc.Summary.Withheld)
 	}
-	if doc := document(api + "/view?hide=system_prompt"); doc.Summary.Withheld["tool_schemas"] != schemas || doc.Summary.Withheld["system_prompt"] != prompts {
-		t.Fatalf("a request did not add to the instance's setting: %v", doc.Summary.Withheld)
+	if rec := get(api + "/view?hide=system_prompt"); rec.Code != http.StatusBadRequest {
+		t.Fatalf("a request named another set on an instance that does not take hide: %d", rec.Code)
+	}
+	if err := srv.SetHide([]string{sessiondata.FlagToolSchemas}, true); err != nil {
+		t.Fatal(err)
 	}
-	if err := srv.SetHide([]string{"finished"}); err == nil {
+	if doc := document(api + "/view"); doc.Summary.Withheld["tool_schemas"] != schemas {
+		t.Fatalf("a request with no hide on an instance that takes it: %v", doc.Summary.Withheld)
+	}
+	if doc := document(api + "/view?hide=system_prompt"); fmt.Sprint(keys(doc.Summary.Withheld)) != "[provider_bodies system_prompt]" || doc.Summary.Withheld["system_prompt"] != prompts {
+		t.Fatalf("a request's hide did not replace the instance's setting: %v", doc.Summary.Withheld)
+	}
+	if doc := document(api + "/view?hide="); len(doc.Summary.Withheld) != 0 || doc.Summary.CapturedPrompts == 0 {
+		t.Fatalf("hide= with no name withheld %v", doc.Summary.Withheld)
+	}
+	if r := get(fmt.Sprintf("%s/files?seq=%d&hide=", api, body)); r.Code != http.StatusOK {
+		t.Fatalf("a provider body file was not served to a request that withholds nothing: %d", r.Code)
+	}
+	if err := srv.SetHide([]string{"finished"}, false); err == nil {
 		t.Fatal("the instance accepted a flag the page cannot withhold")
 	}
 
@@ -232,7 +263,7 @@ func TestAViewWithholdsByFlag(t *testing.T) {
 			transcript = filepath.Join(out, string(scenario.FormatClaudeCode), filepath.FromSlash(f.File))
 		}
 	}
-	if err := srv.SetHide(nil); err != nil {
+	if err := srv.SetHide(nil, false); err != nil {
 		t.Fatal(err)
 	}
 	if err := os.Chmod(transcript, 0o644); err != nil {
@@ -298,6 +329,11 @@ func TestReadersOfDifferentNamesAreServedTogether(t *testing.T) {
 	wg.Wait()
 }
 
+// keys lists a count's names in order.
+func keys(counts map[string]int) []string {
+	return slices.Sorted(maps.Keys(counts))
+}
+
 func edgeIDs(nodes []sessionview.Node) int {
 	n := 0
 	for _, node := range nodes {
diff --git a/tests/langchain_withhold_test.go b/tests/langchain_withhold_test.go
index a49d973..922a3a0 100644
--- a/tests/langchain_withhold_test.go
+++ b/tests/langchain_withhold_test.go
@@ -118,7 +118,11 @@ func TestAnUnfinishedLangChainCallIsWithheld(t *testing.T) {
 	if shows(doc) {
 		t.Fatal("the withheld document shows text read from the withheld record")
 	}
-	h := view_.New(zone, nil).Handler()
+	srv := view_.New(zone, nil)
+	if err := srv.SetHide(nil, true); err != nil {
+		t.Fatal(err)
+	}
+	h := srv.Handler()
 	get := func(path string) *httptest.ResponseRecorder {
 		r := httptest.NewRecorder()
 		h.ServeHTTP(r, httptest.NewRequest(http.MethodGet, path, nil))
@@ -383,7 +387,11 @@ func TestARunsOwnRecordIsCountedWhenWithheld(t *testing.T) {
 	}
 	walk(doc.Talks)
 	walk(doc.Loose)
-	h := view_.New(zone, nil).Handler()
+	srv := view_.New(zone, nil)
+	if err := srv.SetHide(nil, true); err != nil {
+		t.Fatal(err)
+	}
+	h := srv.Handler()
 	get := func(path string) *httptest.ResponseRecorder {
 		r := httptest.NewRecorder()
 		h.ServeHTTP(r, httptest.NewRequest(http.MethodGet, path, nil))