11# Design: Fusion context injection (` context_names ` )
22
3- Status: ** Draft / for review**
3+ Status: ** Draft / for review** (rev 2, incorporates Justin Emerson's live-tested review of rev 1)
44Author: Don Mann
5- Related: Fusion / fleet support, ` Private/Invoke-PfbApiRequest.ps1 ` , capability map (` Data/PfbCapabilityMap.json ` )
5+ Related: Fusion / fleet support, ` Private/Invoke-PfbApiRequest.ps1 ` , capability map
6+ (` Data/PfbCapabilityMap.json ` ), ` Private/Assert-PfbApiCapability.ps1 `
67
78## Problem
89
@@ -26,6 +27,9 @@ non-starter:
2627
2728- Let a caller pick a fleet context once and have it apply to subsequent calls.
2829- Allow a per-call override without a session-wide change.
30+ - ** Never let a set context be silently dropped.** If a caller asks for a context
31+ and it cannot be honored, the call must fail loudly, not silently execute against
32+ the wrong target. (This is the central lesson from the rev-1 review; see below.)
2933- Never send ` context_names ` to an endpoint (or array version) that does not
3034 support it.
3135- Zero change to the ~ 520 public cmdlet signatures.
@@ -36,89 +40,246 @@ non-starter:
3640 This spec covers only injecting ` context_names ` into outbound requests.
3741- ` context_ids ` (id-based targeting). Names first; ids can follow the same shape
3842 if there is demand.
43+ - Client-side write fan-out across multiple contexts (see the multi-value section).
44+
45+ ## What changed since rev 1
46+
47+ Rev 1 proposed storing the ambient override in module script scope and * silently
48+ skipping* injection when the target endpoint did not support ` context_names ` .
49+ Justin's review (live-tested against real arrays, including one on Purity//FB
50+ 4.6.5 / REST 2.22) showed both of those choices reintroduce the exact
51+ silent-wrong-scope failure this feature exists to prevent:
52+
53+ - A ` context_names ` sent to an endpoint that never supported it is ** silently
54+ accepted** by the array (HTTP 200, mutation applied), so "let the array reject
55+ it" does not work as a safety net.
56+ - Script-scoped state does not survive nesting, a second connection, or a
57+ parallel runspace, and would silently fall back to "no context."
58+
59+ Rev 2 fixes both: ** hard-throw instead of silent skip** , and ** connection-object
60+ state instead of module scope** .
3961
4062## Design
4163
42- The module already funnels every request through a single choke point,
43- ` Invoke-PfbApiRequest ` . That is the one place to inject context, so no public
44- cmdlet needs to change.
64+ Every request funnels through one choke point, ` Invoke-PfbApiRequest ` , so that is
65+ the one place context is resolved and injected. No public cmdlet changes.
4566
46- ### 1. Connection-level default
67+ ### 1. Context state lives on the connection object
4768
48- ` Connect-PfbArray ` gains an optional ` -Context <string[]> ` parameter. When
49- supplied, the resolved value is stored on the connection object as
50- ` .DefaultContext ` . This is the "target this fleet member for the whole session"
51- case.
69+ Two properties on the ` $Array ` connection object, with two different mutation
70+ policies:
71+
72+ - ** ` .DefaultContext ` ** -- durable session default. Set at connect time or via
73+ ` Set-PfbContext ` / ` Clear-PfbContext ` , which are ** copy-on-write** : they return
74+ a * new* connection object and never mutate the one the caller passed in. This
75+ prevents a context change leaking into other code holding the same reference.
76+ - ** ` .ContextOverride ` ** -- the ambient, block-scoped value ` Invoke-PfbInContext `
77+ sets. Mutable, but scoped by construction and restored via ` try ` /` finally ` .
78+
79+ Storing this on the object (rather than in ` $script: ` module scope) is what makes
80+ nesting, multiple connections, and ` $using: ` -passed parallel work behave correctly
81+ instead of silently falling back to no context. It also matches how the module
82+ already writes ` AuthToken ` /` TokenExpiresAt ` back onto ` $Array ` during
83+ auto-reconnect. (Distinction: token refresh is a * transparent* mutation; context
84+ is a * targeting* mutation that changes where a write lands -- hence the stricter
85+ copy-on-write policy on the durable default.)
86+
87+ ### 2. Setting a context
5288
5389``` powershell
90+ # At connect (durable session default)
5491$fb = Connect-PfbArray -Endpoint mgmt-fb -ApiToken $t -Context 'member-a'
55- Get-PfbFileSystem -Array $fb # implicitly scoped to member-a
92+
93+ # Change / clear the durable default later (copy-on-write: capture the return value)
94+ $fb = Set-PfbContext -Array $fb -Context 'member-b'
95+ $fb = Clear-PfbContext -Array $fb
96+
97+ # Ambient per-call override (auto-restores, nests correctly)
98+ Invoke-PfbInContext -Array $fb -Context 'member-c' {
99+ Get-PfbFileSystem -Array $fb # member-c
100+ Get-PfbNetworkInterface -Array $fb # member-c
101+ }
102+ Get-PfbFileSystem -Array $fb # back to the session default
56103```
57104
58- ### 2. Per-call override (ambient scope)
105+ ` Set- ` /` Clear-PfbContext ` are ** Phase 1** (moved up from rev 1's Phase 3): without
106+ them a session context set at connect can only be changed by reconnecting. They
107+ must also update the module's connection cache (` $script:PfbArrays ` /
108+ ` $script:PfbDefaultArray ` ) to point at the new copy, or callers relying on the
109+ implicit default connection would keep hitting the old object.
59110
60- Rather than a parameter on every cmdlet, an ambient scope helper sets the context
61- for a block of calls and restores it on exit:
111+ ` Invoke-PfbInContext ` needs no explicit stack -- each invocation captures the
112+ prior value in a local and restores it in ` finally ` , so the PowerShell call stack
113+ provides push/pop discipline and nesting restores the * outer* value (not "none"):
62114
63115``` powershell
64- Invoke-PfbInContext -Array $fb -Context 'member-b' {
65- Get-PfbFileSystem -Array $fb # scoped to member-b
66- Get-PfbNetworkInterface -Array $fb # also member-b
116+ function Invoke-PfbInContext {
117+ param(
118+ [Parameter(Mandatory)][PSCustomObject]$Array,
119+ [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$Context,
120+ [Parameter(Mandatory, Position=0)][scriptblock]$ScriptBlock
121+ )
122+ $previous = $Array.ContextOverride
123+ $Array.ContextOverride = $Context
124+ try { & $ScriptBlock }
125+ finally { $Array.ContextOverride = $previous }
67126}
68- Get-PfbFileSystem -Array $fb # back to the session default
69127```
70128
71- Internally this sets a script-scoped override that ` Invoke-PfbApiRequest ` reads.
72- The scriptblock form guarantees the override is always cleared, even on error.
73- (A ` Set-PfbContext ` / ` Clear-PfbContext ` pair could be offered too, but the
74- scriptblock is the safe default and should ship first.)
129+ ### 3. Resolving and injecting context in ` Invoke-PfbApiRequest `
130+
131+ Resolve the effective context once, at the top of the choke point:
132+
133+ ```
134+ explicit -QueryParams['context_names'] > $Array.ContextOverride > $Array.DefaultContext > none
135+ ```
136+
137+ ** Tri-state "none":** distinguish * unset* (` $null ` ) from * explicit no-context*
138+ (` [string[]]@() ` ). Both resolve to "nothing to inject," but the empty-array form is
139+ a deliberate "run this one call locally" (hence ` [AllowEmptyCollection()] ` and a
140+ ` -ne $null ` check, not a truthiness check). Only a ** non-empty** resolved context
141+ is subject to the hard-throw gate below -- so ` Invoke-PfbInContext -Context @() `
142+ is the escape hatch for running one call locally even on an endpoint that does not
143+ support ` context_names ` .
144+
145+ Then, ** before the existing ` Assert-PfbApiCapability ` call and before the
146+ ` -AutoPaginate ` loop** (ordering matters -- see below), and only when the resolved
147+ context is non-empty:
148+
149+ 1 . If the endpoint's capability-map entry ** lists ` context_names ` ** (any version):
150+ inject it into ` $QueryParams ` and let the existing ` Assert-PfbApiCapability `
151+ version check do its job (unchanged -- it already throws a clean, readable
152+ error when the array's version predates ` context_names ` for that endpoint).
153+ 2 . If the map has ** no entry for ` Method Endpoint ` , or the entry does not list
154+ ` context_names ` ** , AND the connected array's version is ** within the map's
155+ scanned range** (` generatedFrom ` ): ** throw** , naming the endpoint and telling
156+ the caller their context could not be honored. Do not send the request. This
157+ absence is * confirmed* -- we scanned that version's spec and the param is not
158+ there.
159+ 3 . Same as (2) but the array's version ** exceeds** anything the map scanned:
160+ proceed permissively (no inject, no throw). We have no evidence either way, and
161+ the module's founding rule is "a capability check must never block a call that
162+ would otherwise succeed." Backed by the connect-time staleness warning below.
163+ 4 . No context set -> unchanged; no injection, no check.
164+
165+ Apply the throw ** uniformly across all verbs, including ` GET ` ** . A silently
166+ wrong-scoped read is not safer than a blocked write: warnings are routinely
167+ suppressed in automation, a wrong-scope read inside a fleet-wide loop corrupts a
168+ result set invisibly, and a ` GET ` 's output routinely feeds a later mutating call.
169+ ` Invoke-PfbInContext -Context @() ` (or ` Clear-PfbContext ` ) is the explicit opt-out.
75170
76- ### 3. Central injection, gated by the capability map
171+ ** Implementation ordering (two non-obvious details): **
77172
78- In ` Invoke-PfbApiRequest ` , immediately before the request is built:
173+ - Inject ** before** the existing ` Assert-PfbApiCapability ` call, not "immediately
174+ before the request is built." If ` context_names ` lands in ` $QueryParams ` after
175+ Assert runs, Assert never sees it and the version check in step 1 never fires.
176+ - Inject by ** mutating the ` $QueryParams ` hashtable** , not the built query string.
177+ ` -AutoPaginate ` rebuilds the query string from ` $QueryParams ` on every page, so
178+ mutating the hashtable carries ` context_names ` forward for free on every page;
179+ appending to the first page's URI would drop it from page 2 on. (` Get-PfbArraySpace `
180+ is a live example of a paginated read that would be affected.)
79181
80- 1 . Resolve the effective context: per-call override, else connection
81- ` .DefaultContext ` , else none. If none -> do nothing.
82- 2 . If the caller already set ` context_names ` explicitly in ` -QueryParams ` , leave
83- it untouched (explicit wins).
84- 3 . Look up the target ` Method ` + ` Endpoint ` in the capability map. Inject
85- ` context_names ` ** only if** the map says this endpoint accepts it. If the map
86- has no entry for the endpoint, or the endpoint does not list ` context_names ` ,
87- skip injection silently -- fail safe, same philosophy as
88- ` Assert-PfbApiCapability ` .
89- 4 . ` Assert-PfbApiCapability ` (already wired in from the capability-map work) then
90- catches the case where the connected array's version predates ` context_names `
91- for that endpoint, so we do not need a second version check here.
182+ ### 4. Connect-time capability-map staleness warning
183+
184+ Independent of ` context_names ` , the module's capability knowledge can lag a
185+ customer's actual firmware. Surface it once, at ` Connect-PfbArray ` ,
186+ unconditionally (not gated on ` -Context ` ): compare the negotiated ` $Array.ApiVersion `
187+ against the map's ` generatedFrom ` maximum; if the array is newer, cache a flag on
188+ the connection and emit a one-shot warning naming the concrete case, e.g.:
189+
190+ > Connected array is running REST 2.28; this module's capability map only covers
191+ > through REST 2.27 -- capability checks for anything newer, including context
192+ > scoping, cannot be fully verified and may not error even if unsupported. Check
193+ > the PowerShell Gallery for a newer release (` Update-Module ` ).
194+
195+ No live ` Find-Module ` lookup (this module runs against air-gapped arrays); just
196+ point at ` Update-Module ` .
92197
93198### Precedence
94199
95200```
96- explicit -QueryParams['context_names'] > Invoke-PfbInContext override > connection .DefaultContext > (none)
201+ explicit -QueryParams['context_names'] > $Array.ContextOverride > $Array .DefaultContext > (none)
97202```
98203
99- ## Why this is the right shape
204+ ## Multi-value ` context_names ` : a real API split, not a free choice
205+
206+ The OpenAPI spec defines two components that both surface as ` context_names ` :
207+
208+ - ** ` Context_names_get ` ** (` GET ` , plus one ` DELETE ` ): true multi-target fan-out,
209+ comma-separated names, resolved atomically server-side.
210+ - ** ` Context_names ` ** (` POST ` , ` PATCH ` , and almost every ` DELETE ` ): "must be an
211+ array of size 1." The size-1 limit is free-text only, not a structured
212+ ` maxItems ` , so it is not mechanically detectable from the schema.
213+
214+ The split tracks verb almost perfectly. The one exception is
215+ ` DELETE /management-access-policies ` , which uses the multi-value variant and was
216+ also added at exactly REST 2.26 -- possibly a spec/doc error. ** Open question for
217+ Don (below).**
218+
219+ Interim design pending confirmation: treat ` GET ` as multi-context-capable and
220+ ` POST ` /` PATCH ` /` DELETE ` as single-context-only, uniformly, with the one known
221+ ` DELETE ` exception called out in code comments. This also exposes a gap in the
222+ capability map: it records only ` paramName: introducedVersion ` , with no cardinality
223+ concept -- closing it properly means teaching ` tools/Build-PfbCapabilityMap.ps1 `
224+ which of the two spec components an endpoint references (or hardcoding the
225+ verb heuristic with the documented exception).
226+
227+ ## Open questions for Don / Justin to settle
228+
229+ 1 . ** Multi-value on a size-1 write endpoint: throw or client-side fan-out?**
230+ Leaning ** throw** (Justin's recommendation, and mine): client-side fan-out
231+ silently changes the return shape (scalar -> N objects) based on session state
232+ the calling code may not see, has no clean partial-failure story, and is not
233+ equivalent to the server's atomic read-side fan-out. If ergonomics later
234+ demand it, add a separate, explicitly-named opt-in cmdlet that returns
235+ per-context-annotated results and surfaces partial failure loudly -- not
236+ automatic behavior triggered by cmdlet-plus-context-shape.
237+
238+ 2 . ** Is ` DELETE /management-access-policies ` 's multi-value ` context_names ` real
239+ or a spec bug?** Cannot be safely settled by testing (can't tell "honored 2
240+ names" from "silently used the first"). Needs a direct confirmation.
241+
242+ 3 . ** "Explicit ` -QueryParams['context_names'] ` wins" -- wins over what,
243+ concretely?** Today nothing in the public API can populate that top precedence
244+ tier: every public cmdlet builds its own fixed ` QueryParams ` , and
245+ ` Invoke-PfbApiRequest ` is private. Is this reserved for a specific future
246+ fleet-aware cmdlet, or is it defensive layering? If the latter, the doc should
247+ say so rather than imply a path that does not exist yet.
248+
249+ ## Notes for a later pass (non-blocking)
100250
101- - One insertion point (` Invoke-PfbApiRequest ` ), not ~ 520 edits.
102- - Reuses the capability map as the single source of truth for which endpoints
103- accept ` context_names ` , so it stays correct as the map regenerates.
104- - Fail-safe: unknown endpoint or unsupported version -> no injection / clear error,
105- never a silently-wrong request.
106- - Backwards compatible: callers who never set a context see identical behaviour.
251+ - ** Annotate fleet-membership failures with the active context name.** A bad
252+ context name returns ` code 42 "Cannot find array in fleet" ` , which currently
253+ surfaces bare -- no indication of which context (possibly a session default set
254+ and forgotten) caused it. The injection layer should annotate it.
255+ - ** State the security model explicitly.** ` context_names ` is not itself an auth
256+ boundary; any valid management connection can * attempt* to target any fleet
257+ member, and enforcement is entirely server-side (RBAC). The client enforces
258+ nothing about which contexts a caller may target -- say so, so no reader assumes
259+ otherwise.
260+ - ** Pipeline caveat.** ` Invoke-PfbInContext ` 's scriptblock form is not
261+ pipeline-friendly by nature; the durable path is
262+ (` $fb = $fb | Set-PfbContext -Context 'b' ` ).
107263
108- ## Open questions
264+ ## Test plan
109265
110- 1 . Parameter name: ` -Context ` vs ` -ContextNames ` vs ` -FleetMember ` . Leaning
111- ` -Context ` (short, matches the REST concept) with ` -ContextNames ` alias.
112- 2 . Does the capability map already record ` context_names ` per endpoint, or do we
113- need to add that field when it regenerates? (This design assumes it can.)
114- 3 . Multiple contexts: the REST param is plural. Do we support fan-out to several
115- members in one call, or constrain to one for now?
116- 4 . Should ` Get-PfbArrayConnection ` / display surface the active default context?
266+ - Three-level precedence resolution, including the empty-array "explicit none" case.
267+ - Capability-gated injection on both the allow path and the hard-throw path.
268+ - ` Assert-PfbApiCapability ` 's version check for ` context_names ` specifically.
269+ - Nested ` Invoke-PfbInContext ` restoring the * outer* value, including when the
270+ inner block throws (exception safety, not just the happy path).
271+ - Cross-connection isolation: an override set via ` $fb1 ` must not affect calls
272+ made with ` $fb2 ` in the same block.
273+ - ` Set-PfbContext ` leaving the original object's ` .DefaultContext ` untouched while
274+ the cache/default pointer moves to the new copy.
275+ - Staleness warning fires once per connection when the array outranges the map.
117276
118277## Phasing
119278
120- - ** Phase 1** : ` -Context ` on ` Connect-PfbArray ` + central capability-gated
121- injection in ` Invoke-PfbApiRequest ` . Covers the common session-default case.
279+ - ** Phase 1** : connection-object state (` .DefaultContext ` / ` .ContextOverride ` ),
280+ ` Connect-PfbArray -Context ` , ` Set- ` /` Clear-PfbContext ` (copy-on-write),
281+ capability-gated ** hard-throw** injection in ` Invoke-PfbApiRequest ` , and the
282+ connect-time staleness warning.
122283- ** Phase 2** : ` Invoke-PfbInContext ` ambient override.
123- - ** Phase 3** : optional ` Set-PfbContext ` / ` Clear-PfbContext ` , ` context_ids ` ,
124- and display surfacing -- only if asked for .
284+ - ** Phase 3** : ` context_ids ` , and -- only if confirmed wanted -- an explicit
285+ multi-context write fan-out cmdlet .
0 commit comments