Skip to content

Commit 75375a1

Browse files
dmann000claude
andcommitted
docs(design): revise context_names spec per review (rev 2)
Incorporates Justin's live-tested review of rev 1: - Hard-throw instead of silent skip when a non-empty context can't be honored (silent skip was proven to land calls on the local array). - Context state moves to the connection object (.DefaultContext copy-on-write, .ContextOverride ambient) instead of module script scope, fixing nesting / multi-connection / parallel-runspace leakage. - Set-/Clear-PfbContext pulled into Phase 1; Invoke-PfbInContext nests correctly via captured $previous + try/finally. - Injection ordering (before Assert-PfbApiCapability, mutate QueryParams for AutoPaginate), tri-state none, connect-time staleness warning. - Multi-value GET vs size-1 write split documented; 3 open questions flagged for Don to settle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4368577 commit 75375a1

1 file changed

Lines changed: 217 additions & 56 deletions

File tree

Lines changed: 217 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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)
44
Author: 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

Comments
 (0)