@matuto-ai/claude-psst-connector 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  "name": "psst",
9
9
  "source": "./",
10
10
  "description": "Psst messaging for Claude Code",
11
- "version": "0.6.0"
11
+ "version": "0.7.0"
12
12
  }
13
13
  ],
14
14
  "metadata": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psst",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Session-bound Psst agent messaging with explicit receive and acknowledgment. Bounded opt-in native notices; no permission relay.",
5
5
  "author": {
6
6
  "name": "Bruno Orsolon"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Psst for Claude Code
2
2
 
3
- > **Unreleased 0.6.0 migration candidate.** This version moves to `@matuto-ai/claude-psst-connector`. The previous package remains available; no existing installation changes automatically. Use the versioned commands only after the new publication is verified. See [migration checklist](docs/migration-0.6.0.md).
3
+ > **Unreleased 0.7.0 release candidate.** This version moves to `@matuto-ai/claude-psst-connector`. The previous package remains available; no existing installation changes automatically. Use the versioned commands only after the new publication is verified. See [migration checklist](docs/migration-0.6.0.md).
4
4
 
5
5
  Native Claude Code plugin for discovery, send, receive, explicit read acknowledgment,
6
6
  and delivery status through [Psst](https://github.com/matuto-ai/psst).
@@ -22,10 +22,10 @@ supported upgrade workflow and the later owner-machine integration checks.
22
22
 
23
23
  ## Install from GitHub
24
24
 
25
- After release `v0.6.0` is published, install the pinned release:
25
+ After release `v0.7.0` is published, install the pinned release:
26
26
 
27
27
  ```sh
28
- claude plugin marketplace add matuto-ai/claude-psst-connector#v0.6.0
28
+ claude plugin marketplace add matuto-ai/claude-psst-connector#v0.7.0
29
29
  claude plugin install psst@psst-connectors
30
30
  ```
31
31
 
@@ -137,7 +137,7 @@ After npm publication is verified, install the released package into a private
137
137
  local tools directory (reuse your existing prefix when updating):
138
138
 
139
139
  ```sh
140
- npm install --prefix "$HOME/.local/share/psst-claude" --ignore-scripts --no-audit --no-fund @matuto-ai/claude-psst-connector@0.6.0
140
+ npm install --prefix "$HOME/.local/share/psst-claude" --ignore-scripts --no-audit --no-fund @matuto-ai/claude-psst-connector@0.7.0
141
141
  "$HOME/.local/share/psst-claude/node_modules/.bin/psst-claude" doctor
142
142
  ```
143
143
 
@@ -255,3 +255,34 @@ exact event IDs you read with `broker_acknowledge_presence_event`. Notices conta
255
255
  only transport metadata, never message bodies or a reply deadline. They do not
256
256
  resend, cancel, close, or acknowledge any message automatically. See the
257
257
  [durable bounds, recovery and validation limits](docs/presence-notices.md).
258
+
259
+
260
+ ## Explicit reauthorization
261
+
262
+ Use **/psst:reauthorize** to request replacement installation authorization:
263
+ command → confirm the displayed configured HTTPS broker → new public request ID
264
+ → administrator approves that ID at `/admin` → native attachment and selection.
265
+ `/psst:connect` reconnects saved access; it does not request replacement.
266
+
267
+ Pending, approved-but-not-connected, and connected are distinct outcomes.
268
+ Existing working access and old journals/unread messages are retained until the
269
+ candidate attaches and is safely selected. Revoked access is not working access.
270
+ A new credential-bound address never replays the old inbox. Replacement does
271
+ **not** revoke the old credential: manual revocation in Psst admin is a separate
272
+ human action, not a prerequisite. This command never accesses admin automatically.
273
+
274
+ Repeat the command to resume the same pending candidate after cancellation,
275
+ restart or uncertain response. For an approved candidate that failed attachment,
276
+ choose retry attachment or explicitly request a new approval if necessary. Cancellation stops this native
277
+ session's intent only. Preserve state and locks; use the public request ID for
278
+ admin diagnosis. Do not run a separate MCP server or fabricate session binding.
279
+ Missing native tools require `/psst:connect`'s bounded native MCP diagnostics.
280
+
281
+ **Execution cost:** `/psst:reauthorize` is a user-invoked native plugin skill.
282
+ Claude executes its fixed instructions and tool calls in a model turn; it is not
283
+ token-free. Host-owned approval polling does not consume repeated model turns.
284
+ The owner accepted this trade-off for discoverability. The newer Claude Mods
285
+ command API offers deterministic dispatch, but its compatibility with the owner's
286
+ installed host has not been verified and this change does not require it.
287
+
288
+ See [command contract and verification](docs/reauthorization.md).
@@ -9,7 +9,7 @@
9
9
  "source": {
10
10
  "source": "npm",
11
11
  "package": "@matuto-ai/claude-psst-connector",
12
- "version": "0.6.0"
12
+ "version": "0.7.0"
13
13
  },
14
14
  "description": "Psst messaging for Claude Code"
15
15
  }
@@ -1,4 +1,9 @@
1
- # 0.6.0 package and repository migration
1
+ # Package and repository migration for 0.7.0
2
+
3
+ This checklist targets the unreleased **0.7.0** candidate. The historical
4
+ `migration-0.6.0.md` filename is retained so existing links continue to work.
5
+ Older 0.6.0 registry receipts do not establish publication of 0.7.0. Publication
6
+ requires separate owner authorization; a version-bump PR does not publish.
2
7
 
3
8
  This is a prepared, unpublished migration. Old package `@matuto-ai/claude-code-broker-connector` remains available. No release, deprecation, repository rename, account permission or user installation is performed by this change.
4
9
 
@@ -6,12 +11,12 @@ This is a prepared, unpublished migration. Old package `@matuto-ai/claude-code-b
6
11
 
7
12
  1. Review/test this exact candidate and merge it in the existing repository. Regular `verify.yaml` runs syntax checks and focused unit tests in the current repository, without an external fixture checkout.
8
13
  2. The existing GitHub repository is now `matuto-ai/claude-psst-connector`. Confirm checkout and the required `verify` check work under this final URL before publication. Do not rename it again or delete/recreate it. Update local remotes and external references you control.
9
- 3. Create/bootstrap the new public npm package through the owner's supported authenticated npm flow and configure its trusted publisher for owner `matuto-ai`, repository `claude-psst-connector`, workflow `npm-publish.yml`, allowing direct `npm publish`. The old package's trust does not transfer. Follow current npm requirements; never copy a token into source or chat. A first publish used for bootstrap consumes that version: do not attempt to publish `0.6.0` twice. If bootstrap already published the final artifact at `0.6.0`, reconcile that receipt instead of triggering a duplicate publish.
10
- 4. Only after the repository rename and publisher setup, publish GitHub release `v0.6.0` from the reviewed commit, when `0.6.0` has not already been published. This triggers publication of the **new** scoped package. Verify the workflow, package name/version and downloaded artifact before announcing availability.
14
+ 3. Create/bootstrap the new public npm package through the owner's supported authenticated npm flow and configure its trusted publisher for owner `matuto-ai`, repository `claude-psst-connector`, workflow `npm-publish.yml`, allowing direct `npm publish`. The old package's trust does not transfer. Follow current npm requirements; never copy a token into source or chat. A first publish used for bootstrap consumes that version: do not attempt to publish `0.7.0` twice. If bootstrap already published the final artifact at `0.7.0`, reconcile that receipt instead of triggering a duplicate publish.
15
+ 4. Only after the repository rename and publisher setup, publish GitHub release `v0.7.0` from the reviewed commit, when `0.7.0` has not already been published. This triggers publication of the **new** scoped package. Verify the workflow, package name/version and downloaded artifact before announcing availability.
11
16
  5. Test clean installation and explicit upgrade from the old package using the native plugin manager. Keep exactly one active connector and retain all private Psst state. Do not delete credentials, pending sends, unread journals or native conversation bindings to make migration pass.
12
17
  6. Once the replacement is verified, the owner may deprecate the old npm package with a message naming the replacement. Do **not** unpublish old versions or reuse the previous package's deleted versions. Deprecation warns; it neither migrates nor disables installed copies.
13
18
 
14
- Repository and package metadata intentionally point to the final name. Version `0.6.0` under the new npm name is independent of versions under the old name. This is not 1.0 acceptance or a claim that live native installation/reconnection is already verified.
19
+ Repository and package metadata intentionally point to the final name. Version `0.7.0` under the new npm name is independent of versions under the old name. This is not 1.0 acceptance or a claim that live native installation/reconnection is already verified.
15
20
 
16
21
  ## Compatibility boundary
17
22
 
@@ -1,12 +1,12 @@
1
1
  # Distribution and npm publishing
2
2
 
3
- For 0.6.0, follow the [package/repository migration checklist](migration-0.6.0.md) first: verify the final repository URL before releasing and configure the new package's trusted publisher. Do not publish the same name/version twice during bootstrap.
3
+ For installations still on the pre-migration package, follow the [package/repository migration checklist](migration-0.6.0.md) first: verify the final repository URL before releasing and configure the new package's trusted publisher. Do not publish the same name/version twice during bootstrap.
4
4
 
5
5
  The supported native Claude marketplace install works directly from the reviewed
6
6
  GitHub branch/tag. npm is optional; it distributes the same self-contained plugin
7
7
  files and helper CLI, not a separate model client.
8
8
 
9
- The npm package is prepared as `@matuto-ai/claude-psst-connector` version `0.6.0`.
9
+ The npm package is prepared as `@matuto-ai/claude-psst-connector` version `0.7.0`.
10
10
  The version-bump PR itself does not publish. The separately authorized GitHub
11
11
  release triggers the configured trusted publisher; verify its result before
12
12
  announcing availability.
@@ -30,7 +30,7 @@ configured, complete that one-time bootstrap in your authenticated npm environme
30
30
  Do not paste a token into chat or into a committed workflow. The workflow cannot
31
31
  magically create publisher permissions.
32
32
 
33
- After reviewing and merging the PR, create release `v0.6.0` from the reviewed
33
+ After reviewing and merging the PR, create release `v0.7.0` from the reviewed
34
34
  commit. The tag must match package.json. Tests run first, then npm publication
35
35
  uses short-lived OIDC and provenance. Verify the Actions run and npm package
36
36
  version before giving users npm install instructions. Native GitHub installation
@@ -0,0 +1,59 @@
1
+ # Explicit reauthorization
2
+
3
+ `/psst:reauthorize` appears as a user-invoked native plugin skill. It previews the
4
+ configured `broker_url`, asks for target-specific owner confirmation, and calls
5
+ `broker_setup({replace:true,expected_broker_url:origin})`. The expected HTTPS origin
6
+ is checked before any mutation. The primary workflow never reads or passes a
7
+ credential. Normal native MCP tool admission, PreToolUse and session guards remain
8
+ unchanged. Missing native tools are not bypassed by another server or fake binding.
9
+
10
+ Workflow: command → confirm displayed broker → public request ID → administrator
11
+ approves that ID at `/admin` → attachment and safe installation selection.
12
+ `outcome` distinguishes `pending`, `approved_not_connected` and `connected`.
13
+ `connected` may describe old active access; `candidate_connected` describes the
14
+ replacement. Working old access is retained while possible; revoked access is not
15
+ claimed to work. Replacement does not revoke old credentials, and prior revocation
16
+ is not required. Only the human administrator performs revocation in Psst admin.
17
+
18
+ Repeating the command resumes pending candidates after cancellation, restart or
19
+ uncertain POST responses. For approved-but-not-connected candidates, the owner chooses retry attachment
20
+ (without replace) or a new approval (replace:true), including when that candidate
21
+ was revoked before attachment. Errors alone never prove revocation.
22
+ An explicit retry may resend POST with the same durable request ID/key when
23
+ the initial POST never reached the broker; automatic resume/polling stays GET-only.
24
+ `broker_setup_cancel` stops this session's intent only; the durable candidate stays.
25
+ `broker_setup_status` reads state without enrollment. Host polling, not a model
26
+ loop, watches approval. Selection races retain the previous selection and old
27
+ journals/unread messages. New credential-bound addresses never replay old messages.
28
+ Preserve files and locks; do not delete state to recover. Changed target requires
29
+ new confirmation. No startup, reconnect, network-failure or peer-driven enrollment.
30
+
31
+ ## Host dispatch and tokens
32
+
33
+ This release uses the same supported plugin-skill mechanism as `/psst:connect`.
34
+ A skill is a fixed instruction prompt executed by Claude: invocation and tool
35
+ calls consume a model turn. `disable-model-invocation: true` makes it user-only,
36
+ not token-free. The owner accepted this trade-off for command discoverability.
37
+
38
+ Claude's newer [Mods API](https://code.claude.com/docs/en/plugins/mods/api#add-a-command)
39
+ provides native `command.run` and `tool.call` dispatch. Its installed-host
40
+ compatibility has not been verified here. We therefore do not install Mods,
41
+ require new host configuration or claim deterministic/token-free dispatch.
42
+ [Skill reference](https://code.claude.com/docs/en/skills#control-who-invokes-a-skill).
43
+
44
+ ## Verification boundary and command inventory
45
+
46
+ Tests exercise MCP schema/dispatch with fixed arguments and native admission,
47
+ skill packaging/instructions, local approval planning, pending retries,
48
+ cancellation, failed attachment, and selection races. These lightweight tests do
49
+ not represent a live Claude session or live broker authentication. No credentials
50
+ or admin operations are changed by verification.
51
+
52
+ - `/psst:connect`: first setup or reconnect saved authorization; no automatic replacement.
53
+ - `/psst:disconnect`: disconnect this session, without revocation.
54
+ - `/psst:psst`: messaging guidance.
55
+ - `/psst:reauthorize`: deliberate replacement approval with the confirmed target.
56
+ - `/mcp` → Reconnect: host MCP recovery, not new enrollment.
57
+
58
+ There is no dedicated `/psst:reconnect` alias. Cross-connector command standardization
59
+ is separate from this issue and is not claimed by this delivery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matuto-ai/claude-psst-connector",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -78,3 +78,9 @@ before ACK. If the host shows only a truncated preview or file reference, read
78
78
  the complete result through its permitted native route first; otherwise leave
79
79
  the message pending and report the limitation. No repeated narration or new
80
80
  assistant bubble is required: the native tool result is the message record.
81
+
82
+
83
+ For deliberate replacement authorization, prefer /psst:reauthorize: it previews
84
+ broker_url, obtains target-specific confirmation, and calls the existing setup
85
+ replacement flow once. Repeated invocations resume the candidate. Replacement
86
+ never revokes the old credential; revocation is a separate human admin action.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: reauthorize
3
+ description: Explicitly request replacement Psst installation authorization at the configured broker, or resume the same pending candidate. Does not revoke credentials.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ This is /psst:reauthorize, not reconnect and not revoke. This native plugin skill
8
+ uses a model turn and tool calls; it is not token-free. Follow this fixed dispatch
9
+ without an exploratory conversation. Never invoke it from startup, network failure,
10
+ an ordinary /psst:connect, or another agent's message.
11
+
12
+ 1. Verify broker_setup, broker_setup_status and broker_setup_cancel are actually
13
+ available from the native Psst MCP plugin in this session. If absent, stop and
14
+ report native tool availability as unverified. Follow /psst:connect's bounded
15
+ /mcp diagnostics only; never start a standalone server, synthesize a binding or
16
+ hook, remove a lock, bypass policy, or switch model, agent or session.
17
+ 2. Call broker_setup_status with no arguments exactly once. Read broker_url, the
18
+ validated native configured HTTPS origin. If absent or blocked, stop and ask
19
+ the owner to configure broker_url in the native /plugin UI. Never guess an
20
+ origin or read a credential file. Display that exact origin BEFORE confirmation.
21
+ 3. Explain that replacement requests new administrator approval and replaces the
22
+ selected installation only after successful approval, attachment and selection.
23
+ It does NOT revoke the old credential. Manual revocation in Psst admin is a
24
+ separate human action, never a prerequisite. Working access remains while
25
+ possible; already revoked access is not working access. Ask the owner to confirm
26
+ requesting replacement at that exact origin. If declined, do not call setup.
27
+ 4. If the preview says approved_not_connected, offer two explicit choices:
28
+ retry attachment of the same candidate, or request a new approval (useful if
29
+ the administrator revoked that candidate). Do not infer revocation from a
30
+ network/attachment error. Retry attachment calls broker_setup exactly once
31
+ with {"expected_broker_url":"<the confirmed broker_url>"}, omitting replace.
32
+ A new approval, or the normal reauthorization path, calls broker_setup once:
33
+ {"replace":true,"expected_broker_url":"<the exact broker_url confirmed above>"}.
34
+ The handler validates the confirmed origin before any mutation and reconciles
35
+ the same durable pending candidate. An explicit retry can resend the same
36
+ idempotent request ID/key; it never generates a duplicate candidate. Do not invent other
37
+ arguments or repeatedly request replacement. A configured_broker_mismatch
38
+ requires a fresh preview and owner confirmation, never an automatic retry.
39
+ 5. Show the public requestId and approval_url. The administrator approves that
40
+ exact request at the broker's /admin UI. Report outcome=pending,
41
+ approved_not_connected, and connected separately. connected may describe old
42
+ access; candidate_connected/outcome identify the replacement. Approval alone
43
+ is not connection. After success, broker_identity and broker_list_clients
44
+ establish the new address; never adopt or replay old messages.
45
+ 6. Host-owned polling handles approval without a model polling loop. If a response
46
+ is uncertain, report uncertainty; an owner-requested broker_setup_status call
47
+ reads public status without creating another request. Repeating this command
48
+ after restart/cancellation reconciles the same pending candidate. Background
49
+ resume/polling uses GET only; only an explicit owner request can resend the
50
+ idempotent POST when the initial POST may never have reached the broker. broker_setup_cancel
51
+ stops this session's intent and late attachment, not the shared candidate or
52
+ another session. Failed attachment retains the previous selection and old
53
+ journals/unread messages; report approved_not_connected and the sanitized
54
+ error. Preserve all state and locks for recovery.
55
+
56
+ Reconnect (/psst:connect) reuses saved credentials. Reauthorize requests replacement
57
+ approval. Revoke is the human administrator's separate action in Psst admin.
58
+ Credentials must never enter prompts, arguments or logs. No automatic admin access.
@@ -20,7 +20,7 @@ export class ApprovalSession {
20
20
  const current=()=>epoch===this.epoch&&this.isCurrent();
21
21
  try{
22
22
  const previous=this.readApprovalState(options);
23
- const result=await this.beginApproval({...options,...(replace&&previous?{replaceRequestId:previous.requestId}:{})});
23
+ const result=await this.beginApproval({...options,...(replace&&previous&&previous.state!=='pending'?{replaceRequestId:previous.requestId}:{})});
24
24
  if(!current())return {state:'cancelled'};
25
25
  return await this.accept(result,options,epoch);
26
26
  }catch(error){
@@ -15,7 +15,7 @@ const model = object({ provider: { type: ['string', 'null'], maxLength: 128 }, i
15
15
  const metadata = object({ name: text(128), model, project: { type: ['string', 'null'], maxLength: 256 } });
16
16
 
17
17
  export const TOOL_DEFINITIONS = Object.freeze([
18
- {name:'broker_setup',description:'Explicitly request broker administrator approval using the URL already configured natively. Candidate credentials stay private. Set replace only for an explicitly requested replacement; old authorization stays active until approval.',inputSchema:object({replace:{type:'boolean'}})},
18
+ {name:'broker_setup',description:'Explicitly request broker administrator approval using the URL already configured natively. Candidate credentials stay private. Set replace only for an explicitly requested replacement; does not revoke the old credential. Existing pending candidates are resumed.',inputSchema:object({replace:{type:'boolean'},expected_broker_url:text(2048)})},
19
19
  {name:'broker_setup_status',description:'Read local setup and monitor status without network requests or starting enrollment. monitor_observed is a local observation, not task success or a pending user certification step. Never poll this with a model loop.',inputSchema:object()},
20
20
  {name:'broker_setup_cancel',description:'Stop this native session setup polling and late attachment only; does not revoke shared credentials or affect other sessions.',inputSchema:object()},
21
21
  {name:'broker_connect',description:'Connect this confirmed native session using an absolute private pairing or connection JSON file; no raw credentials. Pairing requires a disconnected native session. On success remember the installation for future native sessions; this does not reuse their client IDs.',inputSchema:object({connection_file:text(4096)},['connection_file'])},
@@ -7,6 +7,7 @@ import { PresenceNotices } from './presence-notices.mjs';
7
7
  import { installationRoot, readSelection, saveSelection, configuredBrokerUrl, enforceConfiguredBroker } from './installation-selection.mjs';
8
8
  import { connectPairingFile } from './enrollment-support.mjs';
9
9
  import {switchApproved} from './approval-attachment.mjs';
10
+ import {setupPlan,setupPresentation} from './reauthorization.mjs';
10
11
  import {ApprovalSession} from './approval-session.mjs';
11
12
  import {beginApproval,pollApproval,readApprovalState} from './approval-support.mjs';
12
13
  import {readSetupContext,saveSetupContext} from './setup-context.mjs';
@@ -147,16 +148,20 @@ async function stop(closeSession){connected=false;status();connectionAbort?.abor
147
148
  }
148
149
  }});
149
150
  })});
150
- guarded.broker_setup=async({replace=false}={})=>{
151
+ guarded.broker_setup=async({replace=false,expected_broker_url}={})=>{
151
152
  autoAttempted=true;if(pairing||connecting||approvalSession.busy||approvalTransition)return failure('connection_busy');
152
153
  if(!confirm())return failure('session_binding_unavailable');
153
154
  try{
154
155
  const brokerUrl=configuredBrokerUrl();if(!brokerUrl)return failure('native_setup_configuration_required');
155
- const old=readSetupContext();
156
- if(old&&old.broker_url===brokerUrl&&['requested','pending','approved'].includes(old.state)&&!replace){setupContext=old;}
156
+ if(expected_broker_url!==undefined&&expected_broker_url!==brokerUrl)return failure('configured_broker_mismatch');
157
+ const old=readSetupContext(),options=setupOptions(brokerUrl);
158
+ const plan=setupPlan({replace,context:old,candidate:readApprovalState(options),brokerUrl,selection:readSelection(),expectedBrokerUrl:expected_broker_url});
159
+ if(plan.keepContext){setupContext=old;}
157
160
  else setupContext=saveSetupContext({broker_url:brokerUrl,selection_revision:readSelection()?.revision??null,request_id:null,state:'requested'},old?.revision??null);
158
161
  disabled=false;writePrivateJson(join(directory,'setup-activation.json'),{sessionId:expected.sessionId,lifetime:expected.lifetime,cancelled:false});
159
- return await approvalSession.start(setupOptions(brokerUrl),{replace});
162
+ // Explicit retries may re-POST the same durable ID/key; background resume remains GET-only.
163
+ const result=await approvalSession.start(options,{replace:plan.replace});
164
+ return setupPresentation({brokerUrl,state:result,connected:!!active(),candidateConnected:result.connected===true});
160
165
  }catch(error){
161
166
  // A failed POST may have durable local candidate state. Later startup may GET
162
167
  // it, but never invent a new request or retry POST automatically.
@@ -165,10 +170,15 @@ async function stop(closeSession){connected=false;status();connectionAbort?.abor
165
170
  };
166
171
  const deliveryReadiness=()=>deliveryStatus({connected:!!active(),monitorAttention:monitorAttention()});
167
172
  guarded.broker_setup_status=async()=>{
168
- if(approvalSession.status.state!=='not_started')return {...approvalSession.status,...deliveryReadiness()};
169
- try{const context=readSetupContext();if(!context)return {state:active()?'connected':'setup_required',connected:!!active(),...deliveryReadiness()};
170
- const saved=readApprovalState(setupOptions(context.broker_url));return saved?{state:saved.state,requestId:saved.requestId,requestExpiresAt:saved.requestExpiresAt,connected:!!active(),...deliveryReadiness()}:{state:'setup_required',connected:!!active(),...deliveryReadiness()};
171
- }catch{return {state:'blocked',error:'invalid_saved_setup',connected:!!active(),...deliveryReadiness()};}
173
+ try{
174
+ const brokerUrl=configuredBrokerUrl(),context=readSetupContext();
175
+ const saved=brokerUrl?readApprovalState(setupOptions(brokerUrl)):null;
176
+ const local=approvalSession.status;
177
+ const state=local.state!=='not_started'&&(!local.requestId||local.requestId===saved?.requestId)?local:(saved?{state:saved.state,requestId:saved.requestId,requestExpiresAt:saved.requestExpiresAt}:{state:'setup_required'});
178
+ // An active OLD installation is not evidence that this candidate attached.
179
+ const candidateConnected=!!active()&&saved?.state==='approved'&&context?.broker_url===brokerUrl&&context.state==='applied'&&readSelection()?.connection_file===saved.connectionFile&&connectedConfig?.tokenFile===readPluginConfig(saved.connectionFile).tokenFile;
180
+ return {...setupPresentation({brokerUrl,state,connected:!!active(),candidateConnected}),...deliveryReadiness()};
181
+ }catch{return {state:'blocked',error:'invalid_saved_setup',connected:!!active(),candidate_connected:false,...deliveryReadiness()};}
172
182
  };
173
183
  guarded.broker_setup_cancel=async()=>{approvalSession.cancel();if(approvalTransition&&['stopping','attaching','selecting'].includes(approvalTransition.phase))generation++;if(expected)writePrivateJson(join(directory,'setup-activation.json'),{sessionId:expected.sessionId,lifetime:expected.lifetime,cancelled:true});return {state:'cancelled',existing_connection_retained:!!active()};};
174
184
  guarded.broker_recover_expired=async()=>{
@@ -0,0 +1,14 @@
1
+ // Public command planning only. Credential contents never leave the existing helper.
2
+ export function setupPlan({replace,context,candidate,brokerUrl,selection,expectedBrokerUrl}){
3
+ if(expectedBrokerUrl!==undefined&&expectedBrokerUrl!==brokerUrl)throw Error('configured_broker_mismatch');
4
+ const same=context?.broker_url===brokerUrl;
5
+ const unfinished=same&&['requested','pending','approved'].includes(context.state);
6
+ const resume=replace&&candidate?.state==='pending';
7
+ return {resume,keepContext:unfinished&&(!replace||resume),replace:replace&&!resume};
8
+ }
9
+ export function setupPresentation({brokerUrl,state,connected=false,candidateConnected=false}){
10
+ return {broker_url:brokerUrl,approval_url:brokerUrl?`${brokerUrl}/admin`:null,
11
+ ...Object.fromEntries(Object.entries(state??{}).filter(([key])=>['state','requestId','requestExpiresAt','error','connection','previous_connection_retained'].includes(key))),
12
+ connected, candidate_connected:candidateConnected,
13
+ outcome:state?.state==='pending'?'pending':state?.state==='approved'?(candidateConnected?'connected':'approved_not_connected'):(state?.state??'setup_required')};
14
+ }