@@ -21,8 +21,11 @@ import Toolbar from '../../../src/toolbar/Base.mjs';
2121 * Capability-security boundary (the load-bearing reason this is its own surface): a credential is
2222 * collected only long enough to submit it to the Brain-side Fleet Registry bridge. If that bridge is
2323 * absent, submission fails closed, the PAT field is cleared, and **nothing is stored in the browser /
24- * App Worker** — only a redacted projection reaches the shared `AgentDefinitions` roster, never a
25- * credential byte (mirrors `AgentOS.model.AgentDefinition`'s deliberately credential-free shape).
24+ * App Worker** — only the Brain's canonical redacted response reaches the shared
25+ * `AgentDefinitions` roster, never a credential byte (mirrors
26+ * `AgentOS.model.AgentDefinition`'s deliberately credential-free shape). An accepted definition
27+ * emits `agentDefinitionAccepted`; the Viewport composition root owns the separate Fleet-cockpit
28+ * refresh, so Accounts never maps or reaches into a sibling `FleetAgent` surface.
2629 */
2730class Accounts extends DashboardPanel {
2831 static config = {
@@ -270,9 +273,9 @@ class Accounts extends DashboardPanel {
270273 */
271274 async onAgentConfigIntent ( intent = { } ) {
272275 const
273- me = this ,
274- agentId = intent . id ,
275- bridge = globalThis . AgentOS ?. fleet ?. registryBridge ,
276+ me = this ,
277+ agentId = intent . id ,
278+ bridge = globalThis . AgentOS ?. fleet ?. registryBridge ,
276279 // Neo events add transport-irrelevant envelope fields such as `source`. Reconstruct the
277280 // curated wire intent explicitly so no event metadata can cross the Brain allowlist.
278281 wireIntent = { id : agentId } ;
@@ -444,23 +447,6 @@ class Accounts extends DashboardPanel {
444447 super . destroy ( ...args )
445448 }
446449
447- /**
448- * @summary Create the redacted projection that can safely render in the Body-side roster.
449- * @param {Object } values
450- * @returns {Object } Public agent definition suitable for the shared store; never includes a credential.
451- */
452- createPublicAgentDefinition ( values ) {
453- return {
454- id : values . githubUsername ,
455- githubUsername : values . githubUsername ,
456- harnessType : values . harnessType ,
457- credentialState : 'stored-node-side' ,
458- lifecycleState : 'gated' ,
459- statusText : 'Agent added; lifecycle controls remain gated.' ,
460- updatedAt : new Date ( ) . toISOString ( )
461- }
462- }
463-
464450 /**
465451 * @summary Load a sample public identity without inserting credential bytes.
466452 * @returns {Promise<void> }
@@ -481,7 +467,12 @@ class Accounts extends DashboardPanel {
481467 }
482468
483469 /**
484- * @summary Validate the form, attempt the Brain-side bridge submit, then clear the PAT field.
470+ * @summary Validate the form, attempt the Brain-side bridge submit, and apply only the canonical
471+ * redacted response to the provider-owned AgentDefinitions store. A controlled registry-domain
472+ * rejection renders its reason without mutating Body state; an unexpected or malformed response
473+ * stays sanitized. After an accepted readback, `agentDefinitionAccepted` tells the Viewport
474+ * composition root to refresh the separately-owned Fleet roster from its Brain assembler.
475+ * The PAT field clears after every attempted bridge submit.
485476 * @returns {Promise<void> }
486477 */
487478 async onSubmitAgentClick ( ) {
@@ -504,8 +495,15 @@ class Accounts extends DashboardPanel {
504495 } ;
505496
506497 try {
507- await this . submitToFleetRegistryBridge ( payload ) ;
508- this . upsertPublicAgentDefinition ( this . createPublicAgentDefinition ( payload ) ) ;
498+ const outcome = await this . submitToFleetRegistryBridge ( payload ) ;
499+
500+ if ( outcome ?. status === 'rejected' ) {
501+ this . updateBridgeStatus ( 'is-error' , outcome . reason || 'Agent definition was rejected. Nothing was changed.' ) ;
502+ return
503+ }
504+
505+ this . upsertPublicAgentDefinition ( outcome , payload . credential ) ;
506+ this . fire ( 'agentDefinitionAccepted' , { agent : outcome } ) ;
509507 this . updateBridgeStatus ( 'is-live' , 'Agent added. PAT was not retained in the app worker.' )
510508 } catch ( error ) {
511509 this . updateBridgeStatus ( 'is-error' , 'Could not add agent. Nothing was stored in browser state; PAT field was cleared.' )
@@ -545,7 +543,8 @@ class Accounts extends DashboardPanel {
545543 * app has no Brain-side bridge object, so the view fails closed instead of inventing browser
546544 * persistence.
547545 * @param {Object } payload
548- * @returns {Promise<*> }
546+ * @returns {Promise<Object> } Canonical public agent definition on acceptance, or a controlled
547+ * `{status:'rejected', reason}` domain outcome.
549548 */
550549 async submitToFleetRegistryBridge ( payload ) {
551550 const bridge = globalThis . AgentOS ?. fleet ?. registryBridge ;
@@ -577,17 +576,36 @@ class Accounts extends DashboardPanel {
577576 }
578577
579578 /**
580- * @summary Write the redacted projection into the shared roster store (the Viewport-provider-
581- * hosted `AgentDefinitions` instance this view binds), replacing any prior row for the same
582- * agent. The Fleet view's grid (bound to the same provider store) re-renders reactively — no
583- * cross-view reference is needed.
584- * @param {Object } definition
579+ * @summary Validate and write the Brain's canonical redacted response into the Viewport-owned
580+ * `AgentDefinitions` store. Required public identity fields and the exact submitted credential
581+ * are checked before mutation; a malformed or echoing response fails closed. Existing records
582+ * update in place, while a new definition becomes the selected Accounts resident. This is the
583+ * configuration projection only — the separate FleetAgent roster refreshes through the
584+ * Viewport-owned `agentDefinitionAccepted` composition seam.
585+ * @param {Object } definition Canonical public definition returned by the Brain bridge.
586+ * @param {String } submittedCredential Ephemeral PAT used only to reject an accidental echo.
585587 */
586- upsertPublicAgentDefinition ( definition ) {
587- const store = this . agentDefinitionsStore ;
588+ upsertPublicAgentDefinition ( definition , submittedCredential ) {
589+ const
590+ store = this . agentDefinitionsStore ,
591+ hasTopLevelSecret = definition && [ 'authorization' , 'credential' , 'password' , 'pat' , 'token' ]
592+ . some ( key => Object . hasOwn ( definition , key ) ) ;
593+
594+ let serializedDefinition ;
595+
596+ try {
597+ serializedDefinition = JSON . stringify ( definition )
598+ } catch ( error ) { /* invalid response */ }
599+
600+ if ( ! store || ! definition ?. id || ! definition . githubUsername || ! definition . harnessType ||
601+ ! serializedDefinition || hasTopLevelSecret ||
602+ ( submittedCredential && serializedDefinition . includes ( submittedCredential ) ) ) {
603+ throw new Error ( 'Fleet Registry returned an invalid public agent definition' )
604+ }
605+
606+ const record = store . get ( definition . id ) ;
588607
589- store . remove ( definition . id ) ;
590- store . add ( definition ) ;
608+ record ? record . set ( definition ) : store . add ( definition ) ;
591609
592610 // a just-added agent becomes the scoped one — the operator configures it next
593611 this . selectedAgentId = definition . id
0 commit comments