Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bridges/codex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type CodexConfig struct {
Command string `yaml:"command"`
Listen string `yaml:"listen"`
HomeBaseDir string `yaml:"home_base_dir"`
TrackedPaths []string `yaml:"tracked_paths"`
DefaultModel string `yaml:"default_model"`
NetworkAccess *bool `yaml:"network_access"`
ClientInfo *CodexClientInfo `yaml:"client_info"`
Expand All @@ -40,6 +41,7 @@ codex:
enabled: true
command: "codex"
listen: ""
tracked_paths: []
default_model: "gpt-5.1-codex"
network_access: true
client_info:
Expand Down
17 changes: 9 additions & 8 deletions bridges/codex/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ const (
)

type PortalMetadata struct {
Title string `json:"title,omitempty"`
Slug string `json:"slug,omitempty"`
IsCodexRoom bool `json:"is_codex_room,omitempty"`
CodexThreadID string `json:"codex_thread_id,omitempty"`
CodexCwd string `json:"codex_cwd,omitempty"`
ElevatedLevel string `json:"elevated_level,omitempty"`
AwaitingCwdSetup bool `json:"awaiting_cwd_setup,omitempty"`
ManagedImport bool `json:"managed_import,omitempty"`
Title string `json:"title,omitempty"`
Slug string `json:"slug,omitempty"`
PortalKind string `json:"portal_kind,omitempty"`
WorkspaceRoot string `json:"workspace_root,omitempty"`
IsCodexRoom bool `json:"is_codex_room,omitempty"`
CodexThreadID string `json:"codex_thread_id,omitempty"`
CodexCwd string `json:"codex_cwd,omitempty"`
ElevatedLevel string `json:"elevated_level,omitempty"`
ManagedImport bool `json:"managed_import,omitempty"`
}
Comment on lines 32 to 42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all references to AwaitingCwdSetup in the codebase

rg -n 'AwaitingCwdSetup' --type=go

Repository: beeper/agentremote

Length of output: 748


🏁 Script executed:

cat bridges/codex/metadata.go

Repository: beeper/agentremote

Length of output: 4408


Critical: Removed AwaitingCwdSetup field breaks existing code.

The AwaitingCwdSetup field was removed from PortalMetadata, but it's still referenced in multiple locations that will cause compilation failures:

  • directory_manager.go:20 reads it in isWelcomeCodexPortal()
  • directory_manager.go:184 and 407 write to this field
  • client.go:497 and 1722 check this field in conditionals
  • backfill.go:226 and 255 use this field
  • metadata_test.go:80 references it in a test

Either restore the field or update all usages to use an alternative approach (e.g., PortalKind == "welcome" if that's the replacement).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/metadata.go` around lines 32 - 42, Restore the removed
AwaitingCwdSetup field on PortalMetadata to avoid breaking callers: add a bool
field named AwaitingCwdSetup with the json tag `awaiting_cwd_setup,omitempty` to
the PortalMetadata struct so existing reads/writes (e.g., isWelcomeCodexPortal,
directory_manager.go write sites, client.go conditionals, backfill.go uses, and
metadata_test.go) compile unchanged; alternatively, if you prefer changing
behavior, update all call sites (isWelcomeCodexPortal, directory_manager.go,
client.go, backfill.go, metadata_test.go) to use the new condition (e.g.,
PortalKind == "welcome") consistently instead of referencing AwaitingCwdSetup.


type MessageMetadata struct {
Expand Down
19 changes: 13 additions & 6 deletions bridges/codex/portal_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
)

func codexWelcomePortalKey(loginID networkid.UserLoginID, slug string) (networkid.PortalKey, error) {
slug = strings.TrimSpace(slug)
if slug == "" {
return networkid.PortalKey{}, fmt.Errorf("empty welcome slug")
}
func codexWelcomePortalKey(loginID networkid.UserLoginID) (networkid.PortalKey, error) {
return networkid.PortalKey{
ID: networkid.PortalID(fmt.Sprintf("codex:%s:welcome:%s", loginID, url.PathEscape(slug))),
ID: networkid.PortalID(fmt.Sprintf("codex:%s:welcome", loginID)),
Receiver: loginID,
}, nil
}
Comment on lines +11 to 16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all calls to codexWelcomePortalKey

rg -n 'codexWelcomePortalKey\(' --type=go -A 2

Repository: beeper/agentremote

Length of output: 581


Critical: Function signature change breaks existing call site.

The codexWelcomePortalKey function signature changed to accept only loginID, but directory_manager.go:167 still calls it with two arguments:

portalKey, err := codexWelcomePortalKey(cc.UserLogin.ID, generateShortID())

This will cause a compilation error. Update the call site to pass only the loginID argument.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/portal_keys.go` around lines 11 - 16, The call site still
passes two arguments to codexWelcomePortalKey but the function now only accepts
loginID; update the call in directory_manager.go (the call to
codexWelcomePortalKey(cc.UserLogin.ID, generateShortID())) to pass only
cc.UserLogin.ID (remove the generateShortID() argument) so the call matches the
codexWelcomePortalKey(loginID networkid.UserLoginID) signature.

Expand All @@ -35,3 +31,14 @@ func codexThreadPortalKey(loginID networkid.UserLoginID, threadID string) (netwo
Receiver: loginID,
}, nil
}

func codexWorkspacePortalKey(loginID networkid.UserLoginID, root string) (networkid.PortalKey, error) {
root = strings.TrimSpace(root)
if root == "" {
return networkid.PortalKey{}, fmt.Errorf("empty workspace root")
}
return networkid.PortalKey{
ID: networkid.PortalID(fmt.Sprintf("codex:%s:workspace:%s", loginID, url.PathEscape(root))),
Receiver: loginID,
}, nil
}
279 changes: 279 additions & 0 deletions bridges/codex/workspaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package codex

import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"

"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"

"github.com/beeper/agentremote"
bridgesdk "github.com/beeper/agentremote/sdk"
)

const (
codexPortalKindWelcome = "welcome"
codexPortalKindWorkspaceSpace = "workspace_space"
codexPortalKindChat = "chat"
)

func normalizeTrackedWorkspaceRoots(paths []string) []string {
if len(paths) == 0 {
return nil
}
out := make([]string, 0, len(paths))
seen := make(map[string]struct{}, len(paths))
for _, path := range paths {
path = strings.TrimSpace(path)
if path == "" {
continue
}
clean := filepath.Clean(path)
if clean == "." {
continue
}
if _, ok := seen[clean]; ok {
continue
}
seen[clean] = struct{}{}
out = append(out, clean)
}
if len(out) == 0 {
return nil
}
slices.Sort(out)
return out
}

func workspaceContains(root, cwd string) bool {
root = filepath.Clean(strings.TrimSpace(root))
cwd = filepath.Clean(strings.TrimSpace(cwd))
if root == "." || cwd == "." || root == "" || cwd == "" {
return false
}
if root == cwd {
return true
}
if root == string(filepath.Separator) {
return strings.HasPrefix(cwd, root)
}
return strings.HasPrefix(cwd, root+string(filepath.Separator))
}

func longestMatchingWorkspaceRoot(roots []string, cwd string) string {
cwd = strings.TrimSpace(cwd)
best := ""
for _, root := range roots {
root = strings.TrimSpace(root)
if !workspaceContains(root, cwd) {
continue
}
if len(root) > len(best) {
best = root
}
}
return best
}

func trackedWorkspaceRootsFromConfig(cfg *CodexConfig) []string {
if cfg == nil {
return nil
}
cfg.TrackedPaths = normalizeTrackedWorkspaceRoots(cfg.TrackedPaths)
return slices.Clone(cfg.TrackedPaths)
}

func setTrackedWorkspaceRoots(meta *UserLoginMetadata, roots []string) {
if meta == nil {
return
}
meta.ManagedPaths = normalizeManagedCodexPaths(roots)
}

func (cc *CodexClient) trackedWorkspaceRoots() []string {
return managedCodexPaths(loginMetadata(cc.UserLogin))
}

func (cc *CodexClient) workspaceRootForCwd(cwd string) string {
return longestMatchingWorkspaceRoot(cc.trackedWorkspaceRoots(), cwd)
}

func (cc *CodexClient) allCodexPortals(ctx context.Context) ([]*bridgev2.Portal, error) {
if cc == nil || cc.UserLogin == nil || cc.UserLogin.Bridge == nil || cc.UserLogin.Bridge.DB == nil {
return nil, nil
}
userPortals, err := cc.UserLogin.Bridge.DB.UserPortal.GetAllForLogin(ctx, cc.UserLogin.UserLogin)
if err != nil {
return nil, err
}
out := make([]*bridgev2.Portal, 0, len(userPortals))
for _, userPortal := range userPortals {
if userPortal == nil {
continue
}
portal, err := cc.UserLogin.Bridge.GetExistingPortalByKey(ctx, userPortal.Portal)
if err != nil || portal == nil {
continue
}
meta := portalMeta(portal)
if meta == nil || !meta.IsCodexRoom {
continue
}
out = append(out, portal)
}
return out, nil
}

func isWelcomeCodexPortal(meta *PortalMetadata) bool {

Check failure on line 132 in bridges/codex/workspaces.go

View workflow job for this annotation

GitHub Actions / Package Smoke

isWelcomeCodexPortal redeclared in this block

Check failure on line 132 in bridges/codex/workspaces.go

View workflow job for this annotation

GitHub Actions / Lint

isWelcomeCodexPortal redeclared in this block

Check failure on line 132 in bridges/codex/workspaces.go

View workflow job for this annotation

GitHub Actions / Package Smoke

isWelcomeCodexPortal redeclared in this block

Check failure on line 132 in bridges/codex/workspaces.go

View workflow job for this annotation

GitHub Actions / Lint

isWelcomeCodexPortal redeclared in this block
return meta != nil && meta.IsCodexRoom && meta.PortalKind == codexPortalKindWelcome
}
Comment on lines +141 to +143
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: isWelcomeCodexPortal is redeclared.

This function is already defined in directory_manager.go:19-21:

func isWelcomeCodexPortal(meta *PortalMetadata) bool {
	return meta != nil && meta.IsCodexRoom && meta.AwaitingCwdSetup
}

This redeclaration causes a compilation error as confirmed by the static analysis tool.

The existing function uses AwaitingCwdSetup while this new version uses PortalKind == codexPortalKindWelcome. The existing declaration should be updated or removed rather than adding a duplicate.

🧰 Tools
🪛 GitHub Check: Package Smoke

[failure] 132-132:
isWelcomeCodexPortal redeclared in this block

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/workspaces.go` around lines 132 - 134, There are two
conflicting declarations of isWelcomeCodexPortal; remove the duplicate and keep
a single canonical implementation (do not redeclare the function in
bridges/codex/workspaces.go). Update the existing isWelcomeCodexPortal
(currently in directory_manager.go) to reflect the intended logic: combine the
checks on meta.IsCodexRoom and whichever condition is correct
(meta.AwaitingCwdSetup or meta.PortalKind == codexPortalKindWelcome) so it
covers both use-cases if needed; reference the function name
isWelcomeCodexPortal and the PortalMetadata fields IsCodexRoom,
AwaitingCwdSetup, PortalKind and the constant codexPortalKindWelcome when making
the single shared version.


func isWorkspaceSpacePortal(meta *PortalMetadata) bool {
return meta != nil && meta.IsCodexRoom && meta.PortalKind == codexPortalKindWorkspaceSpace
}

func isCodexChatPortal(meta *PortalMetadata) bool {
return meta != nil && meta.IsCodexRoom && meta.PortalKind == codexPortalKindChat
}

func (cc *CodexClient) composeWorkspaceSpaceInfo(root string) *bridgev2.ChatInfo {
title := codexTitleForPath(root)
if title == "" {
title = root
}
info := agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
Title: title,
Login: cc.UserLogin,
HumanUserIDPrefix: cc.HumanUserIDPrefix,
BotUserID: codexGhostID,
BotDisplayName: "Codex",
CanBackfill: false,
})
if info != nil {
info.Type = ptr.Ptr(database.RoomTypeSpace)
info.Topic = ptr.Ptr(codexTopicForPath(root))
}
return info
}

func (cc *CodexClient) workspaceSpaceForRoot(ctx context.Context, root string) (*bridgev2.Portal, error) {
if cc == nil || cc.UserLogin == nil || cc.UserLogin.Bridge == nil {
return nil, fmt.Errorf("login unavailable")
}
root = strings.TrimSpace(root)
if root == "" {
return nil, nil
}
portalKey, err := codexWorkspacePortalKey(cc.UserLogin.ID, root)
if err != nil {
return nil, err
}
return cc.UserLogin.Bridge.GetPortalByKey(ctx, portalKey)
}

func (cc *CodexClient) ensureWorkspaceSpace(ctx context.Context, root string) (*bridgev2.Portal, error) {
root = strings.TrimSpace(root)
if root == "" {
return nil, fmt.Errorf("workspace root is required")
}
portal, err := cc.workspaceSpaceForRoot(ctx, root)
if err != nil {
return nil, err
}
meta := portalMeta(portal)
meta.IsCodexRoom = true
meta.PortalKind = codexPortalKindWorkspaceSpace
meta.WorkspaceRoot = root
meta.CodexCwd = root
meta.CodexThreadID = ""
meta.ManagedImport = false
meta.Title = codexTitleForPath(root)
meta.Slug = codexThreadSlug(root)
portal.RoomType = database.RoomTypeSpace
portal.OtherUserID = codexGhostID
portal.Name = meta.Title
portal.NameSet = true
portal.Topic = codexTopicForPath(root)
portal.TopicSet = true
_, err = bridgesdk.EnsurePortalLifecycle(ctx, bridgesdk.PortalLifecycleOptions{
Login: cc.UserLogin,
Portal: portal,
ChatInfo: cc.composeWorkspaceSpaceInfo(root),
SaveBeforeCreate: true,
AIRoomKind: agentremote.AIRoomKindAgent,
ForceCapabilities: true,
})
if err != nil {
return nil, err
}
return portal, portal.Save(ctx)
}

func (cc *CodexClient) attachChatToWorkspaceSpace(ctx context.Context, portal *bridgev2.Portal, root string) error {
if portal == nil {
return fmt.Errorf("portal unavailable")
}
meta := portalMeta(portal)
if meta == nil || !isCodexChatPortal(meta) {
return nil
}
root = strings.TrimSpace(root)
meta.WorkspaceRoot = root
if err := portal.Save(ctx); err != nil {
return err
}
info := cc.composeCodexChatInfo(portal, codexPortalTitle(portal), true)
if info != nil {
if root != "" {
space, err := cc.ensureWorkspaceSpace(ctx, root)
if err != nil {
return err
}
info.ParentID = ptr.Ptr(space.PortalKey.ID)
} else {
info.ParentID = nil
}
}
bridgesdk.RefreshPortalLifecycle(ctx, bridgesdk.PortalLifecycleOptions{
Login: cc.UserLogin,
Portal: portal,
ChatInfo: info,
AIRoomKind: agentremote.AIRoomKindAgent,
ForceCapabilities: true,
})
cc.syncCodexRoomTopic(ctx, portal, meta)
return nil
}

func (cc *CodexClient) deleteWorkspaceSpace(ctx context.Context, root string) error {
portal, err := cc.workspaceSpaceForRoot(ctx, root)
if err != nil || portal == nil {
return err
}
meta := portalMeta(portal)
if meta == nil || !isWorkspaceSpacePortal(meta) {
return nil
}
cc.deletePortalOnly(ctx, portal, "codex workspace removed")
return nil
}

func resolveExistingDirectory(raw string) (string, error) {
path, err := resolveCodexWorkingDirectory(raw)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", err
}
if !info.IsDir() {
return "", fmt.Errorf("%s is not a directory", path)
}
return path, nil
}
Loading