Suggested severity
High (S2): the dedicated code-only update path can silently change a published blueprint's connection contract, or fail for an unrelated binding-annotation error.
Summary
The Overseer.updateBlueprint() API documents updateCode and updateBindings as independent options. However, calling updateBlueprint(id, { updateCode: true }) currently calls collectBindingMetadata() and assigns its result to record.metadata.bindings.
The Workshop's Update code action invokes exactly this code-only option, so the normal UI workflow is affected.
Deterministic reproduction on unpatched main
Using commit 8b08672050e6957987a87b5f9cab2801ba89b42c:
- Create a blueprint record whose stored binding metadata is
OLD_API.
- Make the source gadget's current binding metadata return
NEW_API.
- Call
updateBlueprint("blueprint-1", { updateCode: true }).
- Inspect
record.metadata.bindings and the collectBindingMetadata() call count.
Actual result
collectBindingMetadata() is called once and the stored metadata changes from OLD_API to NEW_API, even though updateBindings was not requested.
If collection throws because a current binding annotation is incomplete, the code-only update rejects before snapshotting the code. The controlled regression test failed on unpatched main with promise rejected "Error: invalid current binding annotation" instead of resolving.
Expected result
A code-only update should update the code snapshot/version and preserve the blueprint's existing binding metadata. Binding metadata should be read and replaced only when updateBindings: true is present.
Impact
- A user choosing Update code can unintentionally change the inputs/connections required by an already published blueprint.
- An invalid current binding annotation can block an otherwise valid code-only update.
- The behavior contradicts the public RPC contract, which assigns binding refresh to the independent
updateBindings option.
Root cause
updateBlueprint() enters one shared if (options.updateCode || options.updateBindings) block and unconditionally collects and assigns bindings before checking options.updateCode.
Proposed fix
Keep the shared gadget-ID resolution, but guard collection and assignment with if (options.updateBindings). Cover code-only, bindings-only, and combined updates.
Suggested severity
High (S2): the dedicated code-only update path can silently change a published blueprint's connection contract, or fail for an unrelated binding-annotation error.
Summary
The
Overseer.updateBlueprint()API documentsupdateCodeandupdateBindingsas independent options. However, callingupdateBlueprint(id, { updateCode: true })currently callscollectBindingMetadata()and assigns its result torecord.metadata.bindings.The Workshop's Update code action invokes exactly this code-only option, so the normal UI workflow is affected.
Deterministic reproduction on unpatched
mainUsing commit
8b08672050e6957987a87b5f9cab2801ba89b42c:OLD_API.NEW_API.updateBlueprint("blueprint-1", { updateCode: true }).record.metadata.bindingsand thecollectBindingMetadata()call count.Actual result
collectBindingMetadata()is called once and the stored metadata changes fromOLD_APItoNEW_API, even thoughupdateBindingswas not requested.If collection throws because a current binding annotation is incomplete, the code-only update rejects before snapshotting the code. The controlled regression test failed on unpatched
mainwithpromise rejected "Error: invalid current binding annotation" instead of resolving.Expected result
A code-only update should update the code snapshot/version and preserve the blueprint's existing binding metadata. Binding metadata should be read and replaced only when
updateBindings: trueis present.Impact
updateBindingsoption.Root cause
updateBlueprint()enters one sharedif (options.updateCode || options.updateBindings)block and unconditionally collects and assigns bindings before checkingoptions.updateCode.Proposed fix
Keep the shared gadget-ID resolution, but guard collection and assignment with
if (options.updateBindings). Cover code-only, bindings-only, and combined updates.