feat(tinyplace): add own-profile editing (name/bio/avatar) to Agent World (Closes #4930)#4996
Conversation
…orld
ProfilesSection was display-only: the users.updateProfile write path was
plumbed at every layer (apiClient → RPC handler → SDK update_profile) but no
component ever called it, so name/bio/avatar were read-only.
Add a ProfileEditForm (own profile only) wired to
apiClient.users.updateProfile(cryptoId, { displayName, bio, avatarEmail }).
Writable fields are prefilled from the authoritative users.get record —
including avatarEmail, which the display-side GqlProfile does not carry (the
shown avatar URL is derived server-side from the Gravatar email). On success
the profile refetches through the existing reload path so saved values render.
New agentWorld.profile.* i18n keys added to en.ts and all 13 locales (no em
dashes). Three regression tests cover the edit button, the save round-trip
(updateProfile called + refetch shows the new value), and the save-failure path.
Closes tinyhumansai#4930
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe Agent World profile card now lets the viewer edit their GraphQL profile. The form loads and saves display name, bio, and avatar email, validates required input, handles errors and asynchronous prefilling, reloads saved data, and includes translations across supported locales. ChangesOwn profile editing
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Viewer
participant AgentProfileCard
participant ProfileEditForm
participant apiClient.users
Viewer->>AgentProfileCard: Select Edit profile
AgentProfileCard->>ProfileEditForm: Render edit form
ProfileEditForm->>apiClient.users: get authoritative profile
apiClient.users-->>ProfileEditForm: Return profile fields
Viewer->>ProfileEditForm: Edit and save fields
ProfileEditForm->>apiClient.users: updateProfile
apiClient.users-->>ProfileEditForm: Return save result
ProfileEditForm->>AgentProfileCard: Notify successful save
AgentProfileCard->>AgentProfileCard: Refetch and reload profile
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4381263188
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…4930-tinyplace-profile-editing
|
Synced onto current
@coderabbitai review Re-run on current main; prior failure was an unrelated ObsidianVaultSection flake. |
|
✅ Action performedReview finished.
|
graycyrus
left a comment
There was a problem hiding this comment.
Good, well-scoped fix for #4930 - closes a real gap where the write path was fully plumbed (apiClient -> RPC -> SDK) but never called from any component. i18n coverage across all 13 locales is complete and consistent, tests cover the happy path, the edit-button gate, and the save-failure path, and the avatarEmail-omitted-when-empty logic is a thoughtful touch (avoids silently blanking an existing avatar).
One real bug and one thing I want confirmed before this ships:
[major] Prefill race can silently discard what the user just typed. In ProfileEditForm, displayName/bio/avatarEmail are seeded from the passed-in props, then a useEffect kicks off apiClient.users.get(cryptoId) and unconditionally overwrites those same state vars when it resolves. If the user opens the form and starts typing before that fetch lands (slow network, anything realistic), their edits get clobbered the moment the fetch finishes - no warning, just silently reverted. The tests only pass because they explicitly waitFor the prefill to land before typing, so this doesn't get caught. Fix: track whether the field has been touched (e.g. a touched ref/flag per field, or just don't overwrite if the current value already differs from initialDisplayName/initialBio), or disable the form until the prefetch resolves so there's no window for it.
[major] Worth confirming before merge: handle_tinyplace_users_update_profile (manifest.rs) takes a client-supplied cryptoId with no check that it matches the caller's own wallet/signer identity - unlike the marketplace handlers in the same file, which derive identity from signer.agent_id() rather than trusting client input. That's pre-existing backend code this PR doesn't touch, but per the linked issue this write path was previously dead - this PR is what makes it reachable from a real UI for the first time. If nothing elsewhere in the stack (auth middleware, RPC bridge trust boundary) enforces that the cryptoId being updated belongs to the caller, this is an IDOR: any caller of this RPC could update any user's profile by passing their cryptoId. Might be a non-issue if the RPC bridge is only reachable by the app's own trusted bundle, but given this PR is the one activating the path in production, I'd want that confirmed rather than assumed. Not blocking on the frontend code itself, but flagging because "no security implications" in the PR description isn't quite right if this holds.
Minor: no loading state while the users.get prefetch is in flight - the form briefly shows the display-card values before jumping to the authoritative ones. Not a big deal, but a subtle flash worth a follow-up.
Requesting changes for the prefill race - that's a real data-loss bug in new code. Please also confirm the update_profile authorization question above.
| .get(cryptoId) | ||
| .then(user => { | ||
| if (cancelled) return; | ||
| if (typeof user?.displayName === 'string') setDisplayName(user.displayName); |
There was a problem hiding this comment.
[major] This unconditionally overwrites displayName/bio/avatarEmail with the users.get() result once it resolves - including if the user already started typing in the form while the fetch was in flight. Their edits get silently discarded with no indication anything happened.
Suggestion: only apply the prefetch result if the field hasn't been touched yet (e.g. compare against the initial value, or track a touched flag per field), or disable the inputs until this resolves so there's no window for the race.
There was a problem hiding this comment.
Fixed in f53b99ed0. ProfileEditForm now tracks a per-field touched ref (ProfilesSection.tsx:206); the users.get prefill only writes fields the user hasn't edited (:216-221), and each onChange marks its field touched (:283/299/315). Regression test a late prefill fetch does not clobber what the user already typed defers users.get, types into the display-name field before it resolves, then resolves with a different record and asserts the typed value survives (would fail before this change). Also (3d917bb3f) the display name is now required so it can't be blanked on save.
…4930-tinyplace-profile-editing
…dits ProfileEditForm seeds displayName/bio/avatarEmail from the displayed profile, then a users.get() prefetch overwrote them unconditionally when it resolved — silently clobbering anything the user typed while the fetch was in flight. Track a per-field touched ref and only apply the prefetch to untouched fields. Add a regression test that types before a deferred prefill resolves and asserts the edit survives.
|
Thanks @graycyrus — both points addressed. [major] Prefill race — FIXED. [major] update_profile authorization — confirmed NOT an IDOR. I traced the write path: the RPC handler ( Re:
@coderabbitai review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== PR metadata and changed files ==='
gh pr view 4996 --repo tinyhumansai/openhuman --json baseRefName,headRefName,files --jq '.baseRefName, .headRefName, (.files[] | "\(.path)\t+\(.additions)/-\(.deletions)")'
echo '=== Changed-file outlines ==='
fd -a 'ProfilesSection\.(tsx|test\.tsx)$|en\.ts$' app/src | sort | while IFS= read -r f; do
echo "--- $f"
ast-grep outline "$f" --items all --type function,class --view compact 2>/dev/null || true
done
echo '=== Profile form and API call references ==='
rg -n -C 4 --glob 'app/src/**/*.{ts,tsx}' \
'ProfileEditForm|updateProfile\s*\(|users\.get\s*\(|touched|agentWorld\.profile' app/src
echo '=== Diff against PR base ==='
base="$(git merge-base "origin/$(gh pr view 4996 --repo tinyhumansai/openhuman --json baseRefName --jq .baseRefName)" HEAD)"
git diff --no-ext-diff --unified=80 "$base"...HEAD -- \
app/src/agentworld/pages/ProfilesSection.tsx \
app/src/agentworld/pages/ProfilesSection.test.tsx \
app/src/i18nLength of output: 142753 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== ProfilesSection: edit form and parent reload wiring ==='
sed -n '170,340p' app/src/agentworld/pages/ProfilesSection.tsx
sed -n '440,590p' app/src/agentworld/pages/ProfilesSection.tsx
echo '=== ProfilesSection edit tests ==='
sed -n '500,660p' app/src/agentworld/pages/ProfilesSection.test.tsx
echo '=== TypeScript update-profile API and update type ==='
ast-grep outline app/src/lib/agentworld/invokeApiClient.ts --items all --type interface,type,function --match 'User|Profile|updateProfile' --view compact || true
rg -n -C 8 --glob 'app/src/lib/agentworld/invokeApiClient.ts' \
'updateProfile\s*[:(]|avatarEmail|interface User|type User|UserProfile' app/src/lib/agentworld/invokeApiClient.ts
echo '=== Rust RPC handler and SDK update/signing path (when sources are available) ==='
fd -a 'manifest.rs|users.rs|user.rs' src vendor 2>/dev/null | sort | while IFS= read -r f; do
if rg -q 'update_profile|user_profile_signature_payload|UserProfileUpdate' "$f"; then
echo "--- $f"
rg -n -C 10 'update_profile|user_profile_signature_payload|UserProfileUpdate|avatar_email' "$f"
fi
doneLength of output: 22170 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== Tinyplace submodule metadata ==='
git ls-tree HEAD vendor/tinyplace || true
if [ -f .gitmodules ]; then
cat .gitmodules
fi
echo '=== Available local references to avatar-email update semantics ==='
rg -n -C 6 --hidden \
'avatar[Ee]mail|avatar_email|UserProfileUpdate|user_profile_signature_payload' \
. \
-g '!app/src/agentworld/pages/ProfilesSection.tsx' \
-g '!app/src/agentworld/pages/ProfilesSection.test.tsx' \
-g '!**/node_modules/**' \
-g '!**/.git/**' || true
echo '=== Git remotes configured for the checked-out repository ==='
git remote -vLength of output: 22984 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="tinyhumansai/tiny.place"
ref="39ab493f51818ca940c3b80243c768ffcb90c744"
echo '=== Locate user-update source at the pinned Tinyplace revision ==='
gh api "repos/${repo}/git/trees/${ref}?recursive=1" --paginate \
--jq '.tree[] | select(.type == "blob") | select(.path | test("(^|/)(users|user)\\.rs$")) | .path' \
| sort -u
echo '=== Read likely update/signature implementations ==='
for path in \
"sdk/rust/src/api/users.rs" \
"sdk/rust/src/types/user.rs"
do
echo "--- $path"
gh api "repos/${repo}/contents/${path}?ref=${ref}" --jq '.content' 2>/dev/null \
| tr -d '\n' | base64 -d 2>/dev/null \
| rg -n -C 12 'update_profile|UserProfileUpdate|avatar_email|user_profile_signature_payload|sign' \
|| echo "(file unavailable or contains no matching implementation)"
doneLength of output: 14710
One remaining issue:
✏️ Learnings added
|
oxoxDev
left a comment
There was a problem hiding this comment.
Clean UI-only fill of an already-plumbed write path; i18n parity complete (9 keys × 14 locales) and all checks green — BLOCKED is the required review, not a red check.
Blocking:
- Prefill overwrites in-progress edits (
ProfilesSection.tsx~197-210): theusers.getprefill effect unconditionally callssetDisplayName/setBio/setAvatarEmailwhen it resolves, so if the fetch is slow and the user types first, their edits are silently discarded. Guard with a dirty/touched flag, or seed only the fields not yet edited. (= graycyrus / Codex @212/214.)
Non-blocking:
- Edit is hidden on the directory-fallback path (
isGraphql && cryptoId@465) — an owner resolved viadirectory.reversecan't edit. Confirm the fallback truly lacks a cryptoId. (= Codex @465.) avatarEmailis omitted-when-empty, so it can never be cleared once set, whiledisplayName/bioalways send trimmed — clearing the name blanks it. Asymmetric; consider a min-length guard on name.
displayName/bio are always sent trimmed, so an empty name would blank the existing one, while avatarEmail is omitted-when-empty (never cleared). Make the name required: guard handleSave and disable Save when the display name is blank, with a translated message. Add a regression test and the agentWorld.profile.nameRequired key across all locales.
|
Thanks @oxoxDev. As with the sibling PRs, the review landed on a stale commit — the blocking item was already fixed before your pass. Blocking — prefill overwrites in-progress edits: ALREADY FIXED (commit Non-blocking — name can be blanked (asymmetric clearing): FIXED (this push). A display name is now required: Non-blocking — edit hidden on the directory-fallback path: confirmed the fallback does not lack a cryptoId —
@coderabbitai review |
|
✏️ Learnings added
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/src/agentworld/pages/ProfilesSection.tsx (1)
211-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefill fetch only logs on failure.
The coding guideline requires verbose diagnostics covering entry/exit and external calls for new/changed flows.
handleSavehas entry/success/failure logs, but this prefill effect only logs the failure path — no entry or success log for theapiClient.users.getcall.As per path instructions, "New or changed flows must include verbose, grep-friendly diagnostics covering entry/exit, branches, external calls, retries/timeouts, state transitions, and errors."♻️ Suggested addition
useEffect(() => { let cancelled = false; + log('prefilling profile fields for %s', cryptoId); void apiClient.users .get(cryptoId) .then(user => { if (cancelled) return; + log('prefill from users.get succeeded'); if (!touched.current.displayName && typeof user?.displayName === 'string')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/agentworld/pages/ProfilesSection.tsx` around lines 211 - 229, Update the prefill useEffect keyed by cryptoId to add grep-friendly diagnostics for entry, the apiClient.users.get external call, and successful completion, while preserving the existing cancellation checks and failure logging. Ensure logs distinguish the prefill flow and include relevant cryptoId context without changing state behavior.Source: Path instructions
app/src/agentworld/pages/ProfilesSection.test.tsx (1)
510-625: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the profile-edit suite into a focused test file.
The additions bring this file to 625 lines. Move this suite into a co-located file such as
ProfilesSection.profile-edit.test.tsx.As per coding guidelines, TypeScript files should remain approximately 500 lines or fewer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/agentworld/pages/ProfilesSection.test.tsx` around lines 510 - 625, Extract the entire “own-profile editing” describe suite from ProfilesSection.test.tsx into a co-located ProfilesSection.profile-edit.test.tsx file. Preserve all tests, mocks, imports, and behavior, relocating any shared setup or symbols required for the suite to run independently, and remove the suite from the original file so it stays within the approximate 500-line guideline.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/agentworld/pages/ProfilesSection.test.tsx`:
- Around line 556-586: Update the test around ProfilesSection’s deferred
usersGet prefill so it first waits for an untouched field, such as the avatar
email input, to contain the resolved deferred value. Only after that
prefill-application evidence is observed should it assert that the edited
display name still equals “My New Name.”
- Around line 520-554: Extend the test around ProfilesSection’s save flow to
prefill an existing avatar email, clear the editable avatar email before saving,
and assert that updateProfile is called with SOLANA_ADDR and avatarEmail: ''.
Keep the existing name and bio assertions intact while ensuring the cleared
value is explicitly covered.
---
Nitpick comments:
In `@app/src/agentworld/pages/ProfilesSection.test.tsx`:
- Around line 510-625: Extract the entire “own-profile editing” describe suite
from ProfilesSection.test.tsx into a co-located
ProfilesSection.profile-edit.test.tsx file. Preserve all tests, mocks, imports,
and behavior, relocating any shared setup or symbols required for the suite to
run independently, and remove the suite from the original file so it stays
within the approximate 500-line guideline.
In `@app/src/agentworld/pages/ProfilesSection.tsx`:
- Around line 211-229: Update the prefill useEffect keyed by cryptoId to add
grep-friendly diagnostics for entry, the apiClient.users.get external call, and
successful completion, while preserving the existing cancellation checks and
failure logging. Ensure logs distinguish the prefill flow and include relevant
cryptoId context without changing state behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fb98915b-25b4-4551-a825-eb1ef418fe3f
📒 Files selected for processing (16)
app/src/agentworld/pages/ProfilesSection.test.tsxapp/src/agentworld/pages/ProfilesSection.tsxapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.ts
| test('saves name + bio via users.updateProfile, then refetches so saved values show', async () => { | ||
| graphqlUser | ||
| .mockResolvedValueOnce(makeProfile({ displayName: 'Agent Alice', bio: 'Old bio' })) | ||
| .mockResolvedValue(makeProfile({ displayName: 'Agent Alice v2', bio: 'New bio' })); | ||
| usersGet.mockResolvedValue({ | ||
| cryptoId: SOLANA_ADDR, | ||
| actorType: 'agent', | ||
| displayName: 'Agent Alice', | ||
| bio: 'Old bio', | ||
| emailVerified: false, | ||
| createdAt: '', | ||
| updatedAt: '', | ||
| } as unknown as Awaited<ReturnType<typeof apiClient.users.get>>); | ||
|
|
||
| render(<ProfilesSection />); | ||
| fireEvent.click(await screen.findByRole('button', { name: /edit profile/i })); | ||
|
|
||
| const nameInput = await screen.findByRole('textbox', { name: /display name/i }); | ||
| // Prefilled from users.get (the authoritative writable record). | ||
| await waitFor(() => expect(nameInput).toHaveValue('Agent Alice')); | ||
| const bioInput = screen.getByRole('textbox', { name: /^bio$/i }); | ||
|
|
||
| fireEvent.change(nameInput, { target: { value: 'Agent Alice v2' } }); | ||
| fireEvent.change(bioInput, { target: { value: 'New bio' } }); | ||
| fireEvent.click(screen.getByRole('button', { name: /^save$/i })); | ||
|
|
||
| await waitFor(() => | ||
| expect(updateProfile).toHaveBeenCalledWith( | ||
| SOLANA_ADDR, | ||
| expect.objectContaining({ displayName: 'Agent Alice v2', bio: 'New bio' }) | ||
| ) | ||
| ); | ||
| // Refetch renders the saved values. | ||
| expect(await screen.findByText('Agent Alice v2')).toBeInTheDocument(); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add coverage for clearing an existing avatar email.
This test only asserts name and bio, so omitting a blank avatarEmail still passes. Prefill an existing email, clear it, save, and require:
expect(updateProfile).toHaveBeenCalledWith(
SOLANA_ADDR,
expect.objectContaining({ avatarEmail: '' })
);Based on the PR objectives, clearing the editable avatar email is a known unprotected contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/agentworld/pages/ProfilesSection.test.tsx` around lines 520 - 554,
Extend the test around ProfilesSection’s save flow to prefill an existing avatar
email, clear the editable avatar email before saving, and assert that
updateProfile is called with SOLANA_ADDR and avatarEmail: ''. Keep the existing
name and bio assertions intact while ensuring the cleared value is explicitly
covered.
| test('a late prefill fetch does not clobber what the user already typed (#4930)', async () => { | ||
| graphqlUser.mockResolvedValue(makeProfile({ displayName: 'Agent Alice', bio: 'Old bio' })); | ||
| // Defer users.get so it resolves AFTER the user has started editing. | ||
| let resolveGet!: (value: unknown) => void; | ||
| const deferred = new Promise<unknown>(res => { | ||
| resolveGet = res; | ||
| }); | ||
| usersGet.mockReturnValue(deferred as unknown as ReturnType<typeof apiClient.users.get>); | ||
|
|
||
| render(<ProfilesSection />); | ||
| fireEvent.click(await screen.findByRole('button', { name: /edit profile/i })); | ||
|
|
||
| const nameInput = await screen.findByRole('textbox', { name: /display name/i }); | ||
| // User types before the prefill lands. | ||
| fireEvent.change(nameInput, { target: { value: 'My New Name' } }); | ||
|
|
||
| // Prefill resolves late with the authoritative (different) record. | ||
| resolveGet({ | ||
| cryptoId: SOLANA_ADDR, | ||
| actorType: 'agent', | ||
| displayName: 'Agent Alice', | ||
| bio: 'Old bio', | ||
| avatarEmail: 'alice@example.com', | ||
| emailVerified: false, | ||
| createdAt: '', | ||
| updatedAt: '', | ||
| }); | ||
|
|
||
| // The user's edit survives; the prefill must not overwrite the touched field. | ||
| await waitFor(() => expect(nameInput).toHaveValue('My New Name')); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Wait for evidence that the delayed prefill was applied.
Line 585 can pass immediately while the deferred promise’s state update is still pending. First wait until an untouched field—such as avatar email—contains the deferred value, then assert that the touched name remains unchanged.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/agentworld/pages/ProfilesSection.test.tsx` around lines 556 - 586,
Update the test around ProfilesSection’s deferred usersGet prefill so it first
waits for an untouched field, such as the avatar email input, to contain the
resolved deferred value. Only after that prefill-application evidence is
observed should it assert that the edited display name still equals “My New
Name.”
|
@graycyrus @oxoxDev — resolved. [major] Prefill race (data loss) → FIXED ( update_profile authorization (possible IDOR) → confirmed client-side + filed backend tracker #5024. I did not assume; I traced it:
Bonus — name can be blanked (oxoxDev, asymmetric clearing) → FIXED (
|
Summary
apiClient.users.updateProfilewrite path.users.getrecord — includingavatarEmail, which the display-sideGqlProfiledoes not carry (the shown avatar URL is derived server-side from the Gravatar email).agentWorld.profile.*i18n keys added toen.tsand all 13 locale files (no em dashes). No SDK/contract change —vendor/tinyplaceis untouched.Problem
Per the §8 Profiles audit (#4776),
app/src/agentworld/pages/ProfilesSection.tsxread name/bio/avatar but had no edit form —grep -r "updateProfile" app/srcreturned onlyinvokeApiClient.ts+ its.test.ts, never a production caller. The save round-trip existed at every layer and was simply never invoked:apiClient.users.updateProfile(cryptoId, update)(app/src/lib/agentworld/invokeApiClient.ts:1746)handle_tinyplace_users_update_profile(src/openhuman/tinyplace/manifest.rs:400, registeredschemas.rs:171)UsersApi::update_profile()(vendor/tinyplace/sdk/rust/src/api/users.rs:35), writingUserProfileUpdate { display_name, bio, avatar_email, link, tags, … }(types/user.rs:148)Read-back is fully available: the
Usertype (types/user.rs:16) carries the same fields,update_profilereturns the updatedUser, and the display path (apiClient.graphql.user→GqlProfile) reflects saved values on refetch. So — unlike the feed-media case (#4924) — the contract is complete and this is a UI-only gap.Solution
ProfileEditForminProfilesSection.tsx, shown only for the graphql profile source (which carries an editable profile +cryptoId; the section always renders the wallet's own profile, so editing is inherently own-only).users.getsoavatarEmail(absent fromGqlProfile) is authoritative.apiClient.users.updateProfile(cryptoId, { displayName, bio, avatarEmail? })(avatarEmail sent only when non-empty so an untouched field never blanks an existing avatar), then triggers the existing reload so the saved values display. Errors keep the form open with a translated retry message.agentworld:profilenamespace; values are never logged (no PII).Submission Checklist
pnpm test:coveragenot run locally (machine constraint); CI enforces the gate.N/A: fills an existing feature gap, no new matrix row.## Related.users.updateProfile/users.getRPCs; tests mock the client.N/A: does not touch a release-cut surface.Closes #NNNin the## Relatedsection.Impact
vendor/tinyplacesubmodule pointer unchanged.user.profilepayload (which includescryptoId,vendor/tinyplace/sdk/rust/src/api/users.rs:124) with the caller's wallet signer and presents the signing public key (http.rs:412-428), so a caller can only update the cryptoId whose key they hold; the core uses the local wallet as signer and/rpcis token-gated to the app. The RPC handler passingcryptoIdthrough unchecked is fine on that basis; backend-side enforcement (signer==cryptoId owner) is tracked for confirmation in Verify backend enforces signer==cryptoId on user.profile updates (defense-in-depth, raised on #4996) #5024. Avatar is Gravatar-email-based (the only write contract the SDK exposes), surfaced honestly in the UI.Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/GH-4930-tinyplace-profile-editing438126318Validation Run
pnpm --filter openhuman-app format:check— ran targetedprettier --writeon all touched files (clean).pnpm typecheck— passed clean (exit 0).app/src/agentworld/pages/ProfilesSection.test.tsx(not run locally; CI verifies).Validation Blocked
command:pnpm test:coverage/pnpm testerror:intentionally not run — local full test/build is disabled on this machine (fills disk).impact:CI is the authority for full test + coverage verification.Behavior Changes
Parity Contract
avatarEmailis omitted from the update when empty so an existing avatar is never accidentally blanked.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Localization
Tests