Skip to content

Delegated-cred shard key diverges: relayfile login mints under sha256(workspace UUID), relayfile start/mount reads under sha256(rw_ handle) → mount can never find its cred #321

Description

@khaliqgant

Summary

relayfile login and relayfile start/mount compute the delegated-credentials shard directory off different workspace identifiers for the same workspace, so the cred the login mints is invisible to the mount. The mount then hard-fails:

error: resolve delegated relayfile credentials: delegated relayfile credentials are required:
/Users/<me>/.relayfile/delegated/f96b6a002731a95006862230/12e1c5683dd6145dbf5e21cd.json not found

…even immediately after a successful relayfile login, because login wrote the cred into a different delegated/<workspaceKey>/… directory. This blocks relayfile start/mount entirely (no amount of agent-relay cloud login / relayfile login fixes it; only manual cred surgery does).

This is a regression introduced by the per-(workspace, scope) sharded delegated-cred layout that replaced the old flat delegated-credentials.json (the layout that closed #291's scope-clobber). The sharding key is now inconsistent between the mint path and the resolve path.

Root cause (deterministic, proven)

The shard path is delegated/<workspaceKey>/<scopeKey>.json where (cmd/relayfile-cli/main.go):

func delegatedCredentialsWorkspaceKey(workspaceValue string) string {
    // resolves workspaceValue -> record.ID if found, else uses workspaceValue raw
    sum := sha256.Sum256([]byte(workspaceValue))
    return hex.EncodeToString(sum[:])[:24]
}
  • Mint path (relayfile login, main.go:1776) passes the canonical UUID:
    delegatedCredentialsPathForRequest(record.ID, mintScopes)workspaceKey = sha256("50587328-441d-4acb-b8f3-dbe1b3c5de99")[:24] = 1fcac40f…

  • Resolve/mount path (loadDelegatedCredentialsForRequestresolveDelegatedCredentialsPathForRequest, main.go:7099/7061) passes the raw rw_ handle, because resolveDelegatedCredentialsWorkspaceValue (main.go:7072) returns it without canonicalizing to record.ID:

    func resolveDelegatedCredentialsWorkspaceValue(workspaceValue string) string {
        workspaceValue = strings.TrimSpace(workspaceValue)
        if workspaceValue != "" && !strings.EqualFold(workspaceValue, "active") {
            return workspaceValue          // <-- "rw_7ccfea89" passed through un-resolved
        }
        ...
    }

    workspaceKey = sha256("rw_7ccfea89")[:24] = f96b6a00…

Hash proof

sha256("rw_7ccfea89")[:24]                              = f96b6a002731a95006862230   (what mount reads)
sha256("50587328-441d-4acb-b8f3-dbe1b3c5de99")[:24]     = 1fcac40f2f48ebae8ab4425d   (what login mints)

Both identifiers name the same workspace (relayWorkspaceId: rw_7ccfea89, id: 50587328-…, name: default in ~/.relayfile/workspaces.json). Confirmed live: after relayfile logout && relayfile login, the cred appeared at delegated/1fcac40f…/12e1c568….json, while relayfile start rw_7ccfea89 .integrations kept looking under delegated/f96b6a00…/12e1c568….json.

(Note: delegatedCredentialsWorkspaceKey does attempt workspaceRecordByID, which matches record.RelayWorkspaceID == id — so in principle "rw_7ccfea89" should resolve to the UUID. In the live mount/start path it did not (it hashed the raw handle), so either the catalog isn't loaded in that execution context or the resolve path bypasses it. Canonicalizing in resolveDelegatedCredentialsWorkspaceValue fixes it regardless; the maintainer should also confirm loadWorkspaceCatalog() is populated wherever the key is derived.)

Reproduction

# logged in to the cloud session; workspace "default" has relayWorkspaceId rw_7ccfea89
relayfile logout && relayfile login          # mints delegated/<sha256(UUID)>/<scope>.json
relayfile start rw_7ccfea89 .integrations --background
# -> error: resolve delegated relayfile credentials: ... /delegated/<sha256("rw_7ccfea89")>/<scope>.json not found
ls ~/.relayfile/delegated/                    # two dirs for ONE workspace: sha256(UUID) and sha256(rw_ handle)

Manual workaround that proves the diagnosis (the token is workspace-scoped, not principal-bound):

cp ~/.relayfile/delegated/<sha256(UUID)>/<scope>.json ~/.relayfile/delegated/<sha256(rw_handle)>/
relayfile start rw_7ccfea89 .integrations --background   # now succeeds

Proposed fix

Canonicalize the workspace identifier to the resolved record.ID before hashing, on the resolve/mount path so it matches the mint path. Concretely, have resolveDelegatedCredentialsWorkspaceValue (main.go:7072) resolve rw_ handle / workspace name → record.ID via resolveWorkspaceRecord, and/or make delegatedCredentialsWorkspaceKey the single canonicalization point used by both mint and resolve. Add a regression test asserting delegatedCredentialsPathForRequest("rw_<handle>", …) and delegatedCredentialsPathForRequest("<uuid>", …) produce the same path when both name one catalog record.

Impact

Blocks the factory live writeback E2E: the factory self-starts its writeback mount with relayfile start <rw_handle> .integrations --background, which can never find the delegated cred relayfile login minted. (factory#11's CLI-driven mount self-start depends on this working.)

Related

Found 2026-06-19 running the factory live E2E off merged main; relayfile CLI 0.10.4.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions