customer-map-codex-bridge 0.5.1 → 0.5.3

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.
package/README.md CHANGED
@@ -1,52 +1,64 @@
1
- # Customer Map Codex Bridge
2
-
3
- This local helper connects a user's already-authenticated Codex installation to Customer Map without sending Codex credentials or API keys to Customer Map.
4
-
5
- ## Requirements
6
-
7
- - Codex CLI installed and already signed in locally (`codex login`).
8
- - Optional Gmail actions require `gog` v0.11.0+ with the intended Gmail account already authorized locally.
9
- - Node.js 18+.
10
- - The Customer Map Agent settings panel must provide a one-time `CMAP-CODEX-*` code.
11
-
12
- The settings panel reports **Connected** only after both the local Codex app-server and the Customer Map relay WebSocket are online. A claimed code by itself is only a pending binding.
13
-
14
- ## Run
15
-
1
+ # Customer Map Codex Bridge
2
+
3
+ This local helper connects a user's already-authenticated Codex installation to Customer Map without sending Codex credentials or API keys to Customer Map.
4
+
5
+ ## Requirements
6
+
7
+ - Codex CLI installed and already signed in locally (`codex login`).
8
+ - Optional Gmail actions require `gog` v0.11.0+ with the intended Gmail account already authorized locally.
9
+ - Node.js 18+.
10
+ - The Customer Map Agent settings panel must provide a one-time `CMAP-CODEX-*` code.
11
+
12
+ The settings panel reports **Connected** only after both the local Codex app-server and the Customer Map relay WebSocket are online. A claimed code by itself is only a pending binding.
13
+
14
+ ## Run
15
+
16
16
  ```bash
17
- npx -y customer-map-codex-bridge@0.5.1 \
17
+ npx -y customer-map-codex-bridge@0.5.3 \
18
18
  --site https://your-customer-map.example \
19
19
  --code CMAP-CODEX-XXXXXXXXXXXX
20
20
  ```
21
21
 
22
- Starting a newly authorized Bridge automatically replaces the older Bridge for the same Customer Map account. Clicking **Remove connection** in Customer Map disconnects the running Bridge, so users do not need to find or kill local processes.
23
-
24
- The Bridge automatically reconnects to the Relay after temporary network or Relay interruptions, using exponential backoff from 1 second up to 30 seconds. It keeps the local Codex process and bridge token in memory during retries. A removed connection, an expired token, or a Bridge replaced by a newer Bridge exits instead of retrying.
25
-
26
- The bridge initializes Codex App Server with native live web search enabled, then explicitly applies a read-only sandbox with no approval prompts and command network access disabled to every new thread and every turn. This lets the assistants search and read public pages without granting model-generated shell commands arbitrary network or file-write access. Credentials stay on the local machine, and thread IDs are stored under `~/.customer-map-codex/state.json` and resumed after Bridge restarts. Task Assistant, Workbench Assistant, and Sales Assistant use separate thread scopes.
22
+ ## Click-to-authorize (Windows and macOS)
27
23
 
28
- When the user explicitly chooses **Save Gmail draft** or **Send email** in Customer Map, the bridge bypasses the Codex turn and executes only a validated, fixed `gog gmail drafts create` or `gog gmail send` argument list. It verifies the content hash and requires a real Gmail message/draft ID before reporting success. Chat cannot turn this into an arbitrary shell or network action.
29
-
30
- If the protocol handler cannot find `codex`, pass its absolute path explicitly:
24
+ The Customer Map button can open the local authorization window directly after the protocol handler is registered once. Run this one-time command in a terminal on the computer where Codex is installed:
31
25
 
32
26
  ```bash
33
- npx -y customer-map-codex-bridge@0.5.1 \
34
- --site https://your-customer-map.example \
35
- --code CMAP-CODEX-XXXXXXXXXXXX \
36
- --codex /absolute/path/to/codex
27
+ npx -y customer-map-codex-bridge@0.5.3 --install-protocol
37
28
  ```
38
29
 
39
- If `gog` is outside the standard Homebrew/system paths, pass it explicitly:
40
-
30
+ The installer registers the `customer-map-codex://` URL scheme for the current user and stores the local Codex path. It does not upload credentials or start a Bridge. Return to Customer Map and click **Open Codex authorization**; the native authorization window will appear on Windows or macOS.
31
+
32
+ If the browser still does not open the window, use the **fallback command** shown in Agent settings. This is also the supported path on Linux, where click-to-authorize is not currently registered.
33
+
34
+ Starting a newly authorized Bridge automatically replaces the older Bridge for the same Customer Map account. Clicking **Remove connection** in Customer Map disconnects the running Bridge, so users do not need to find or kill local processes.
35
+
36
+ The Bridge automatically reconnects to the Relay after temporary network or Relay interruptions, using exponential backoff from 1 second up to 30 seconds. It keeps the local Codex process and bridge token in memory during retries. A removed connection, an expired token, or a Bridge replaced by a newer Bridge exits instead of retrying.
37
+
38
+ The bridge initializes Codex App Server with native live web search enabled, then explicitly applies a read-only sandbox with no approval prompts and command network access disabled to every new thread and every turn. This lets the assistants search and read public pages without granting model-generated shell commands arbitrary network or file-write access. Credentials stay on the local machine, and thread IDs are stored under `~/.customer-map-codex/state.json` and resumed after Bridge restarts. Task Assistant, Workbench Assistant, and Sales Assistant use separate thread scopes.
39
+
40
+ When the user explicitly chooses **Save Gmail draft** or **Send email** in Customer Map, the bridge bypasses the Codex turn and executes only a validated, fixed `gog gmail drafts create` or `gog gmail send` argument list. It verifies the content hash and requires a real Gmail message/draft ID before reporting success. Chat cannot turn this into an arbitrary shell or network action.
41
+
42
+ If the protocol handler cannot find `codex`, pass its absolute path explicitly:
43
+
41
44
  ```bash
42
- npx -y customer-map-codex-bridge@0.5.1 \
43
- --site https://your-customer-map.example \
44
- --code CMAP-CODEX-XXXXXXXXXXXX \
45
- --gog /absolute/path/to/gog
46
- ```
47
-
48
- The website can then open:
49
-
50
- ```text
51
- customer-map-codex://connect?site=https%3A%2F%2Fyour-customer-map.example&code=CMAP-CODEX-XXXXXXXXXXXX
45
+ npx -y customer-map-codex-bridge@0.5.3 \
46
+ --site https://your-customer-map.example \
47
+ --code CMAP-CODEX-XXXXXXXXXXXX \
48
+ --codex /absolute/path/to/codex
52
49
  ```
50
+
51
+ If `gog` is outside the standard Homebrew/system paths, pass it explicitly:
52
+
53
+ ```bash
54
+ npx -y customer-map-codex-bridge@0.5.3 \
55
+ --site https://your-customer-map.example \
56
+ --code CMAP-CODEX-XXXXXXXXXXXX \
57
+ --gog /absolute/path/to/gog
58
+ ```
59
+
60
+ The website can then open:
61
+
62
+ ```text
63
+ customer-map-codex://connect?site=https%3A%2F%2Fyour-customer-map.example&code=CMAP-CODEX-XXXXXXXXXXXX
64
+ ```
@@ -0,0 +1,35 @@
1
+ use framework "Cocoa"
2
+ use scripting additions
3
+
4
+ on run argv
5
+ if (count of argv) is less than 1 then return "ERROR"
6
+
7
+ set siteUrl to item 1 of argv
8
+ set alert to current application's NSAlert's alloc()'s init()
9
+ alert's setMessageText:"Connect Customer Map to Codex"
10
+ alert's setInformativeText:("Use the Codex already signed in on this Mac." & return & return & ¬
11
+ "Read-only Codex conversations and public-web search are available for your sales tasks." & return & ¬
12
+ "File writes and model-generated command networking remain blocked." & return & ¬
13
+ "Only explicit Gmail draft or send actions may use your local gog installation.")
14
+ alert's setAlertStyle:(current application's NSInformationalAlertStyle)
15
+
16
+ set siteField to current application's NSTextField's alloc()'s initWithFrame:{{0, 0}, {360, 28}}
17
+ siteField's setStringValue:siteUrl
18
+ siteField's setEditable:false
19
+ siteField's setSelectable:true
20
+ siteField's setBezeled:true
21
+ siteField's setDrawsBackground:true
22
+ siteField's setBackgroundColor:(current application's NSColor's colorWithCalibratedWhite:0.96 alpha:1.0)
23
+ siteField's setTextColor:(current application's NSColor's colorWithCalibratedWhite:0.22 alpha:1.0)
24
+ siteField's setFont:(current application's NSFont's monospacedSystemFontOfSize:11 weight:(current application's NSFontWeightRegular))
25
+ siteField's setLineBreakMode:(current application's NSLineBreakByTruncatingMiddle)
26
+ alert's setAccessoryView:siteField
27
+
28
+ alert's addButtonWithTitle:"Cancel"
29
+ alert's addButtonWithTitle:"Allow connection"
30
+
31
+ current application's NSApp's activateIgnoringOtherApps:true
32
+ set response to alert's runModal()
33
+ if response is (current application's NSAlertSecondButtonReturn) then return "ALLOW"
34
+ return "CANCEL"
35
+ end run
@@ -0,0 +1,300 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$Site,
4
+ [switch]$TestMode,
5
+ [string]$RenderPath = ''
6
+ )
7
+
8
+ $ErrorActionPreference = 'Stop'
9
+
10
+ try {
11
+ Add-Type -AssemblyName PresentationFramework
12
+ Add-Type -AssemblyName PresentationCore
13
+ Add-Type -AssemblyName WindowsBase
14
+
15
+ [xml]$xaml = @'
16
+ <Window
17
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
18
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
19
+ Title="Customer Map - Codex authorization"
20
+ Width="560"
21
+ Height="540"
22
+ WindowStartupLocation="CenterScreen"
23
+ ResizeMode="NoResize"
24
+ ShowActivated="True"
25
+ Topmost="True"
26
+ Background="#F6F5F2"
27
+ FontFamily="Segoe UI"
28
+ UseLayoutRounding="True"
29
+ SnapsToDevicePixels="True">
30
+ <Window.Resources>
31
+ <Style x:Key="QuietButton" TargetType="Button">
32
+ <Setter Property="MinWidth" Value="104"/>
33
+ <Setter Property="Height" Value="38"/>
34
+ <Setter Property="Padding" Value="18,0"/>
35
+ <Setter Property="FontSize" Value="13"/>
36
+ <Setter Property="FontWeight" Value="SemiBold"/>
37
+ <Setter Property="Cursor" Value="Hand"/>
38
+ <Setter Property="Template">
39
+ <Setter.Value>
40
+ <ControlTemplate TargetType="Button">
41
+ <Border
42
+ x:Name="ButtonSurface"
43
+ Background="{TemplateBinding Background}"
44
+ BorderBrush="{TemplateBinding BorderBrush}"
45
+ BorderThickness="1"
46
+ CornerRadius="6"
47
+ RenderTransformOrigin="0.5,0.5">
48
+ <Border.RenderTransform>
49
+ <ScaleTransform ScaleX="1" ScaleY="1"/>
50
+ </Border.RenderTransform>
51
+ <ContentPresenter
52
+ HorizontalAlignment="Center"
53
+ VerticalAlignment="Center"
54
+ RecognizesAccessKey="True"/>
55
+ </Border>
56
+ <ControlTemplate.Triggers>
57
+ <Trigger Property="IsMouseOver" Value="True">
58
+ <Setter TargetName="ButtonSurface" Property="Opacity" Value="0.9"/>
59
+ </Trigger>
60
+ <Trigger Property="IsPressed" Value="True">
61
+ <Setter TargetName="ButtonSurface" Property="Opacity" Value="0.82"/>
62
+ <Setter TargetName="ButtonSurface" Property="RenderTransform">
63
+ <Setter.Value>
64
+ <ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
65
+ </Setter.Value>
66
+ </Setter>
67
+ </Trigger>
68
+ <Trigger Property="IsKeyboardFocused" Value="True">
69
+ <Setter TargetName="ButtonSurface" Property="BorderBrush" Value="#315F7D"/>
70
+ <Setter TargetName="ButtonSurface" Property="BorderThickness" Value="2"/>
71
+ </Trigger>
72
+ <Trigger Property="IsEnabled" Value="False">
73
+ <Setter TargetName="ButtonSurface" Property="Opacity" Value="0.45"/>
74
+ </Trigger>
75
+ </ControlTemplate.Triggers>
76
+ </ControlTemplate>
77
+ </Setter.Value>
78
+ </Setter>
79
+ </Style>
80
+ <Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource QuietButton}">
81
+ <Setter Property="MinWidth" Value="124"/>
82
+ <Setter Property="Background" Value="#1F2328"/>
83
+ <Setter Property="BorderBrush" Value="#1F2328"/>
84
+ <Setter Property="Foreground" Value="#FFFFFF"/>
85
+ </Style>
86
+ <Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource QuietButton}">
87
+ <Setter Property="Background" Value="#FFFFFF"/>
88
+ <Setter Property="BorderBrush" Value="#B7C8D1"/>
89
+ <Setter Property="Foreground" Value="#1F2328"/>
90
+ </Style>
91
+ </Window.Resources>
92
+
93
+ <Grid Margin="12">
94
+ <Border
95
+ Background="#FFFFFF"
96
+ BorderBrush="#E2E8F0"
97
+ BorderThickness="1"
98
+ CornerRadius="8">
99
+ <Grid>
100
+ <Grid.RowDefinitions>
101
+ <RowDefinition Height="3"/>
102
+ <RowDefinition Height="Auto"/>
103
+ <RowDefinition Height="*"/>
104
+ <RowDefinition Height="68"/>
105
+ </Grid.RowDefinitions>
106
+
107
+ <Border Grid.Row="0" Background="#315F7D" CornerRadius="8,8,0,0"/>
108
+
109
+ <Grid Grid.Row="1" Margin="22,18,22,14">
110
+ <Grid.ColumnDefinitions>
111
+ <ColumnDefinition Width="42"/>
112
+ <ColumnDefinition Width="*"/>
113
+ </Grid.ColumnDefinitions>
114
+ <Border
115
+ Width="38"
116
+ Height="38"
117
+ HorizontalAlignment="Left"
118
+ VerticalAlignment="Top"
119
+ Background="#DBE8F0"
120
+ CornerRadius="6">
121
+ <TextBlock
122
+ Text="CM"
123
+ HorizontalAlignment="Center"
124
+ VerticalAlignment="Center"
125
+ Foreground="#1F2328"
126
+ FontSize="12"
127
+ FontWeight="Bold"/>
128
+ </Border>
129
+ <StackPanel Grid.Column="1" Margin="12,0,0,0">
130
+ <TextBlock
131
+ Text="Connect Customer Map to Codex"
132
+ Foreground="#1F2328"
133
+ FontSize="16"
134
+ FontWeight="SemiBold"/>
135
+ <TextBlock
136
+ Margin="0,4,0,0"
137
+ Text="Use the Codex already signed in on this computer."
138
+ Foreground="#667085"
139
+ FontSize="12.5"/>
140
+ </StackPanel>
141
+ </Grid>
142
+
143
+ <StackPanel Grid.Row="2" Margin="22,0,22,18">
144
+ <TextBlock
145
+ Text="REQUESTING WEBSITE"
146
+ Foreground="#667085"
147
+ FontSize="10.5"
148
+ FontWeight="SemiBold"/>
149
+ <Border
150
+ Margin="0,7,0,16"
151
+ Padding="12,10"
152
+ Background="#FBFAF8"
153
+ BorderBrush="#E2E8F0"
154
+ BorderThickness="1"
155
+ CornerRadius="6">
156
+ <TextBlock
157
+ x:Name="SiteText"
158
+ Foreground="#1F2328"
159
+ FontFamily="Consolas"
160
+ FontSize="12"
161
+ TextTrimming="CharacterEllipsis"/>
162
+ </Border>
163
+
164
+ <TextBlock
165
+ Text="WHAT THIS ALLOWS"
166
+ Foreground="#667085"
167
+ FontSize="10.5"
168
+ FontWeight="SemiBold"/>
169
+
170
+ <Grid Margin="0,9,0,0">
171
+ <Grid.ColumnDefinitions>
172
+ <ColumnDefinition Width="18"/>
173
+ <ColumnDefinition Width="*"/>
174
+ </Grid.ColumnDefinitions>
175
+ <Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
176
+ <StackPanel Grid.Column="1">
177
+ <TextBlock Text="Read-only Codex conversations" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
178
+ <TextBlock Text="File writes and model-generated command networking stay blocked." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
179
+ </StackPanel>
180
+ </Grid>
181
+
182
+ <Grid Margin="0,11,0,0">
183
+ <Grid.ColumnDefinitions>
184
+ <ColumnDefinition Width="18"/>
185
+ <ColumnDefinition Width="*"/>
186
+ </Grid.ColumnDefinitions>
187
+ <Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
188
+ <StackPanel Grid.Column="1">
189
+ <TextBlock Text="Live public-web search" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
190
+ <TextBlock Text="Codex may search and read public websites for your sales tasks." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
191
+ </StackPanel>
192
+ </Grid>
193
+
194
+ <Grid Margin="0,11,0,0">
195
+ <Grid.ColumnDefinitions>
196
+ <ColumnDefinition Width="18"/>
197
+ <ColumnDefinition Width="*"/>
198
+ </Grid.ColumnDefinitions>
199
+ <Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
200
+ <StackPanel Grid.Column="1">
201
+ <TextBlock Text="Local credentials stay local" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
202
+ <TextBlock Text="Customer Map never receives your Codex login or API keys." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
203
+ </StackPanel>
204
+ </Grid>
205
+
206
+ <Border
207
+ Margin="0,16,0,0"
208
+ Padding="11,9"
209
+ Background="#E4EBE2"
210
+ CornerRadius="6">
211
+ <TextBlock
212
+ Text="Only explicit Gmail draft or send actions may use your local gog installation."
213
+ Foreground="#33413A"
214
+ FontSize="11.5"
215
+ TextWrapping="Wrap"/>
216
+ </Border>
217
+ </StackPanel>
218
+
219
+ <Border
220
+ Grid.Row="3"
221
+ Background="#FBFAF8"
222
+ BorderBrush="#E2E8F0"
223
+ BorderThickness="0,1,0,0"
224
+ CornerRadius="0,0,8,8">
225
+ <Grid Margin="22,14">
226
+ <Grid.ColumnDefinitions>
227
+ <ColumnDefinition Width="*"/>
228
+ <ColumnDefinition Width="Auto"/>
229
+ <ColumnDefinition Width="10"/>
230
+ <ColumnDefinition Width="Auto"/>
231
+ </Grid.ColumnDefinitions>
232
+ <TextBlock
233
+ VerticalAlignment="Center"
234
+ Text="Manage this connection in Customer Map settings."
235
+ TextTrimming="CharacterEllipsis"
236
+ Foreground="#667085"
237
+ FontSize="10.5"/>
238
+ <Button
239
+ x:Name="CancelButton"
240
+ Grid.Column="1"
241
+ Content="Cancel"
242
+ IsCancel="True"
243
+ Style="{StaticResource SecondaryButton}"/>
244
+ <Button
245
+ x:Name="AllowButton"
246
+ Grid.Column="3"
247
+ Content="Allow connection"
248
+ IsDefault="True"
249
+ Style="{StaticResource PrimaryButton}"/>
250
+ </Grid>
251
+ </Border>
252
+ </Grid>
253
+ </Border>
254
+ </Grid>
255
+ </Window>
256
+ '@
257
+
258
+ $reader = New-Object System.Xml.XmlNodeReader $xaml
259
+ $window = [Windows.Markup.XamlReader]::Load($reader)
260
+ $window.FindName('SiteText').Text = $Site
261
+
262
+ if ($TestMode) {
263
+ if ($RenderPath) {
264
+ $window.WindowStartupLocation = 'Manual'
265
+ $window.Left = -10000
266
+ $window.Top = -10000
267
+ $window.ShowInTaskbar = $false
268
+ $window.Show()
269
+ $window.UpdateLayout()
270
+
271
+ $width = [Math]::Max(1, [int][Math]::Ceiling($window.ActualWidth))
272
+ $height = [Math]::Max(1, [int][Math]::Ceiling($window.ActualHeight))
273
+ $bitmap = New-Object Windows.Media.Imaging.RenderTargetBitmap($width, $height, 96, 96, [Windows.Media.PixelFormats]::Pbgra32)
274
+ $bitmap.Render($window)
275
+ $encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder
276
+ $encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
277
+ $stream = [IO.File]::Open($RenderPath, [IO.FileMode]::Create)
278
+ try { $encoder.Save($stream) } finally { $stream.Dispose() }
279
+ $window.Close()
280
+ }
281
+ exit 0
282
+ }
283
+
284
+ $script:approved = $false
285
+ [void]$window.FindName('AllowButton').Focus()
286
+ $window.FindName('AllowButton').Add_Click({
287
+ $script:approved = $true
288
+ $window.Close()
289
+ })
290
+ $window.FindName('CancelButton').Add_Click({
291
+ $window.Close()
292
+ })
293
+
294
+ [void]$window.ShowDialog()
295
+ if ($script:approved) { exit 0 }
296
+ exit 1
297
+ } catch {
298
+ Write-Error $_
299
+ exit 2
300
+ }
@@ -0,0 +1,43 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { delimiter, dirname } from 'node:path';
3
+ import process from 'node:process';
4
+
5
+ export const CODEX_APP_SERVER_ARGS = [
6
+ 'app-server',
7
+ '--listen',
8
+ 'stdio://',
9
+ '-c',
10
+ 'web_search="live"',
11
+ ];
12
+
13
+ export function startCodexAppServer(cwd, command) {
14
+ const pathValue = [dirname(process.execPath), dirname(command), process.env.PATH || '']
15
+ .filter(Boolean)
16
+ .join(delimiter);
17
+ const options = {
18
+ cwd,
19
+ stdio: ['pipe', 'pipe', 'inherit'],
20
+ windowsHide: true,
21
+ env: { ...process.env, PATH: pathValue },
22
+ };
23
+
24
+ if (process.platform !== 'win32') return spawn(command, CODEX_APP_SERVER_ARGS, options);
25
+
26
+ const commandShell = process.env.ComSpec || process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe';
27
+ // cmd.exe needs one outer pair of quotes around the complete /c command.
28
+ // windowsVerbatimArguments keeps Node from escaping those quotes.
29
+ const commandLine = buildWindowsCodexCommandLine(command, CODEX_APP_SERVER_ARGS);
30
+ return spawn(commandShell, ['/d', '/s', '/c', commandLine], {
31
+ ...options,
32
+ windowsVerbatimArguments: true,
33
+ });
34
+ }
35
+
36
+ export function buildWindowsCodexCommandLine(command, args = CODEX_APP_SERVER_ARGS) {
37
+ return '"' + [command, ...args].map(quoteWindowsArg).join(' ') + '"';
38
+ }
39
+
40
+ function quoteWindowsArg(value) {
41
+ // Within a cmd.exe command, embedded quotes are escaped by doubling them.
42
+ return '"' + String(value).replaceAll('"', '""') + '"';
43
+ }