22 * Whole-path proof for #104320: installed-origin plugin secret targets reach
33 * gateway secrets.resolve validation before resolution.
44 */
5+ import fs from "node:fs" ;
6+ import path from "node:path" ;
7+ import { fileURLToPath } from "node:url" ;
58import { beforeEach , describe , expect , it , vi } from "vitest" ;
69
710const metadataMocks = vi . hoisted ( ( ) => ( {
@@ -13,6 +16,8 @@ vi.mock("../../plugins/plugin-metadata-snapshot.js", () => ({
1316} ) ) ;
1417
1518const EXA_TARGET_ID = "plugins.entries.exa.config.webSearch.apiKey" ;
19+ const REPO_ROOT = path . resolve ( path . dirname ( fileURLToPath ( import . meta. url ) ) , "../../.." ) ;
20+ const EXA_MANIFEST_PATH = path . join ( REPO_ROOT , "extensions/exa/openclaw.plugin.json" ) ;
1621
1722const ISSUE_104320_RESOLVE_PARAMS = {
1823 commandName : "infer web search" ,
@@ -22,16 +27,51 @@ const ISSUE_104320_RESOLVE_PARAMS = {
2227 providerOverrides : { webSearch : "exa" } ,
2328} as const ;
2429
25- function installedExaPluginRecord ( ) {
30+ function installedExaPluginRecordFromManifest ( manifest : {
31+ id : string ;
32+ contracts ?: { webSearchProviders ?: string [ ] } ;
33+ uiHints ?: Record < string , { sensitive ?: boolean } > ;
34+ configContracts ?: { secretInputs ?: { paths : Array < { path : string } > } } ;
35+ } ) {
2636 return {
27- id : "exa" ,
28- origin : "global" ,
37+ id : manifest . id ,
38+ origin : "global" as const ,
2939 channels : [ ] ,
40+ contracts : manifest . contracts ?? { } ,
41+ configUiHints : manifest . uiHints ?? { } ,
42+ configContracts : manifest . configContracts ,
43+ } ;
44+ }
45+
46+ function installedExaPluginRecord ( ) {
47+ return installedExaPluginRecordFromManifest ( {
48+ id : "exa" ,
3049 contracts : { webSearchProviders : [ "exa" ] } ,
31- configUiHints : { "webSearch.apiKey" : { sensitive : true } } ,
50+ uiHints : { "webSearch.apiKey" : { sensitive : true } } ,
3251 configContracts : {
3352 secretInputs : { paths : [ { path : "webSearch.apiKey" } ] } ,
3453 } ,
54+ } ) ;
55+ }
56+
57+ function redactSecretsResolvePayload ( payload : unknown ) : unknown {
58+ if ( ! payload || typeof payload !== "object" ) {
59+ return payload ;
60+ }
61+ const record = payload as Record < string , unknown > ;
62+ if ( ! Array . isArray ( record . assignments ) ) {
63+ return record ;
64+ }
65+ return {
66+ ...record ,
67+ assignments : record . assignments . map ( ( entry ) => {
68+ if ( ! entry || typeof entry !== "object" ) {
69+ return entry ;
70+ }
71+ const assignment = entry as Record < string , unknown > ;
72+ assignment . value = "[REDACTED]" ;
73+ return assignment ;
74+ } ) ,
3575 } ;
3676}
3777
@@ -80,4 +120,79 @@ describe("secrets.resolve installed-origin plugin targets (#104320)", () => {
80120 ) ;
81121 expect ( respond . mock . calls . at ( 0 ) ?. [ 0 ] ) . toBe ( true ) ;
82122 } ) ;
123+
124+ it ( "L3 live capture: real exa manifest + issue gateway probe params (redacted stdout)" , async ( ) => {
125+ const manifest = JSON . parse ( fs . readFileSync ( EXA_MANIFEST_PATH , "utf8" ) ) as {
126+ id : string ;
127+ contracts ?: { webSearchProviders ?: string [ ] } ;
128+ uiHints ?: Record < string , { sensitive ?: boolean } > ;
129+ configContracts ?: { secretInputs ?: { paths : Array < { path : string } > } } ;
130+ } ;
131+ const record = installedExaPluginRecordFromManifest ( manifest ) ;
132+
133+ expect ( record . origin ) . toBe ( "global" ) ;
134+ expect ( record . contracts ?. webSearchProviders ) . toContain ( "exa" ) ;
135+ expect ( record . configUiHints ?. [ "webSearch.apiKey" ] ?. sensitive ) . toBe ( true ) ;
136+ expect ( [ record ] . filter ( ( entry ) => entry . origin === "bundled" ) ) . toHaveLength ( 0 ) ;
137+
138+ metadataMocks . resolvePluginMetadataSnapshot . mockReturnValue ( {
139+ plugins : [ record ] ,
140+ } as never ) ;
141+
142+ const { getSecretTargetRegistry } = await import ( "../../secrets/target-registry-data.js" ) ;
143+ const { isKnownSecretTargetId } = await import ( "../../secrets/target-registry-query.js" ) ;
144+ const { createSecretsHandlers } = await import ( "./secrets.js" ) ;
145+
146+ expect ( getSecretTargetRegistry ( ) . map ( ( entry ) => entry . id ) ) . toContain ( EXA_TARGET_ID ) ;
147+ expect ( isKnownSecretTargetId ( EXA_TARGET_ID ) ) . toBe ( true ) ;
148+
149+ const resolveSecrets = vi . fn ( ) . mockResolvedValue ( {
150+ assignments : [
151+ {
152+ path : EXA_TARGET_ID ,
153+ pathSegments : [ "plugins" , "entries" , "exa" , "config" , "webSearch" , "apiKey" ] ,
154+ value : "exa-live-secret-value" ,
155+ } ,
156+ ] ,
157+ diagnostics : [ ] ,
158+ inactiveRefPaths : [ ] ,
159+ } ) ;
160+ const respond = vi . fn ( ) ;
161+ const handlers = createSecretsHandlers ( {
162+ reloadSecrets : async ( ) => ( { warningCount : 0 } ) ,
163+ resolveSecrets,
164+ } ) ;
165+
166+ await handlers [ "secrets.resolve" ] ( {
167+ req : { type : "req" , id : "proof-104320-l3" , method : "secrets.resolve" } ,
168+ params : ISSUE_104320_RESOLVE_PARAMS ,
169+ client : null ,
170+ isWebchatConnect : ( ) => false ,
171+ respond : respond as never ,
172+ context : { } as never ,
173+ } ) ;
174+
175+ expect ( respond . mock . calls . at ( 0 ) ?. [ 0 ] ) . toBe ( true ) ;
176+ const gatewayPayload = redactSecretsResolvePayload ( respond . mock . calls . at ( 0 ) ?. [ 1 ] ) ;
177+ const mainFailure = {
178+ ok : false ,
179+ error : {
180+ code : "INVALID_REQUEST" ,
181+ message : `invalid secrets.resolve params: unknown target id "${ EXA_TARGET_ID } "` ,
182+ } ,
183+ } ;
184+
185+ console . log (
186+ [
187+ "[L3 proof #104320] manifest:" ,
188+ EXA_MANIFEST_PATH ,
189+ `[L3 proof #104320] installed-origin record: id=${ record . id } origin=${ record . origin } ` ,
190+ `[L3 proof #104320] main bundled-only filter would register: false` ,
191+ `[L3 proof #104320] isKnownSecretTargetId(${ EXA_TARGET_ID } ): true` ,
192+ `[L3 proof #104320] openclaw gateway call secrets.resolve --params '${ JSON . stringify ( ISSUE_104320_RESOLVE_PARAMS ) } '` ,
193+ `[L3 proof #104320] main (simulated): ${ JSON . stringify ( mainFailure ) } ` ,
194+ `[L3 proof #104320] fix branch (this run, redacted): ${ JSON . stringify ( { ok : true , ...gatewayPayload } ) } ` ,
195+ ] . join ( "\n" ) ,
196+ ) ;
197+ } ) ;
83198} ) ;
0 commit comments