Skip to content

feat(tinyplace): add own-profile editing (name/bio/avatar) to Agent World (Closes #4930)#4996

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4930-tinyplace-profile-editing
Jul 18, 2026
Merged

feat(tinyplace): add own-profile editing (name/bio/avatar) to Agent World (Closes #4930)#4996
senamakel merged 5 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4930-tinyplace-profile-editing

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Agent World Profiles was display-only; this adds an Edit profile form (own profile only) for name / bio / avatar, wired to the already-plumbed apiClient.users.updateProfile write path.
  • Writable fields prefill 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 save the profile refetches through the existing reload path so the saved name/bio/avatar render.
  • New agentWorld.profile.* i18n keys added to en.ts and all 13 locale files (no em dashes). No SDK/contract change — vendor/tinyplace is untouched.

Problem

Per the §8 Profiles audit (#4776), app/src/agentworld/pages/ProfilesSection.tsx read name/bio/avatar but had no edit formgrep -r "updateProfile" app/src returned only invokeApiClient.ts + its .test.ts, never a production caller. The save round-trip existed at every layer and was simply never invoked:

  • TS apiClient.users.updateProfile(cryptoId, update) (app/src/lib/agentworld/invokeApiClient.ts:1746)
  • RPC handle_tinyplace_users_update_profile (src/openhuman/tinyplace/manifest.rs:400, registered schemas.rs:171)
  • SDK UsersApi::update_profile() (vendor/tinyplace/sdk/rust/src/api/users.rs:35), writing UserProfileUpdate { display_name, bio, avatar_email, link, tags, … } (types/user.rs:148)

Read-back is fully available: the User type (types/user.rs:16) carries the same fields, update_profile returns the updated User, and the display path (apiClient.graphql.userGqlProfile) reflects saved values on refetch. So — unlike the feed-media case (#4924) — the contract is complete and this is a UI-only gap.

Solution

  • Added ProfileEditForm in ProfilesSection.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).
  • Fields: display name, bio, and avatar email (Gravatar — the actual writable avatar contract; the UI labels it as such rather than faking an image-URL field). Prefilled from users.get so avatarEmail (absent from GqlProfile) is authoritative.
  • Save calls 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.
  • Debug logging under the agentworld:profile namespace; values are never logged (no PII).

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — three new cases (edit button present; save calls updateProfile + refetch shows new value; save-failure keeps form + error).
  • Diff coverage ≥ 80% — the new form/component is exercised by the added tests. pnpm test:coverage not run locally (machine constraint); CI enforces the gate.
  • Coverage matrix updated — N/A: fills an existing feature gap, no new matrix row.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related.
  • No new external network dependencies introduced (mock backend used per Testing Strategy) — uses the existing users.updateProfile/users.get RPCs; tests mock the client.
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: does not touch a release-cut surface.
  • Linked issue closed via Closes #NNN in the ## Related section.

Impact

  • Desktop UI only (Agent World → Profiles). No Rust/core, schema, SDK, or network changes; vendor/tinyplace submodule pointer unchanged.
  • No migration. Security: the write is authorized cryptographically, not by trusting client input — the SDK signs the canonical user.profile payload (which includes cryptoId, 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 /rpc is token-gated to the app. The RPC handler passing cryptoId through 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

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/GH-4930-tinyplace-profile-editing
  • Commit SHA: 438126318

Validation Run

  • pnpm --filter openhuman-app format:check — ran targeted prettier --write on all touched files (clean).
  • pnpm typecheck — passed clean (exit 0).
  • Focused tests: three new cases in app/src/agentworld/pages/ProfilesSection.test.tsx (not run locally; CI verifies).
  • Rust fmt/check (if changed): N/A — no Rust changes.
  • Tauri fmt/check (if changed): N/A — no Tauri changes.

Validation Blocked

  • command: pnpm test:coverage / pnpm test
  • error: 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

  • Intended behavior change: users can edit and save their own name / bio / avatar (Gravatar email) from the Profiles page.
  • User-visible effect: an Edit profile button + form; saved values display after refetch.

Parity Contract

  • Legacy behavior preserved: the display card, wallet-locked / no-handle / payment-required / directory-fallback states are unchanged; editing only appears for the own graphql profile.
  • Guard/fallback/dispatch parity checks: avatarEmail is omitted from the update when empty so an existing avatar is never accidentally blanked.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none.
  • Canonical PR: this.
  • Resolution: N/A.

Summary by CodeRabbit

  • New Features

    • Added profile editing for users’ own Agent World profiles.
    • Users can update their display name, bio, and avatar email.
    • Added validation, save progress feedback, and error handling.
    • Profile data refreshes after successful updates.
  • Localization

    • Added translated profile-editing text across supported languages.
  • Tests

    • Added coverage for editing, validation, delayed data loading, refresh behavior, and save failures.

…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
@M3gA-Mind
M3gA-Mind requested a review from a team July 16, 2026 13:47
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5578a6b2-ea5c-4787-96fd-894509877cad

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The 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.

Changes

Own profile editing

Layer / File(s) Summary
Profile edit form and localization
app/src/agentworld/pages/ProfilesSection.tsx, app/src/lib/i18n/*
ProfileEditForm loads profile fields, protects typed values from late prefills, validates and trims display names, saves updates, and renders localized controls and status messages.
Profile card integration and validation
app/src/agentworld/pages/ProfilesSection.tsx, app/src/agentworld/pages/ProfilesSection.test.tsx
The own GraphQL profile gains an edit toggle and post-save reload flow; tests cover rendering, saving, delayed prefills, failures, and blank-name validation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: feature, agent

Suggested reviewers: graycyrus

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
Loading

Poem

I’m a bunny with a profile to tune,
I trim my name beneath the moon.
Bio and avatar hop in line,
Save the changes—everything’s fine!
If errors thump, the form stays near.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: own-profile editing for Agent World profiles.
Linked Issues check ✅ Passed The PR adds own-profile editing UI wired to updateProfile, refetches after save, and covers name/bio/avatar editing as requested in #4930.
Out of Scope Changes check ✅ Passed The changes stay within the requested profile-editing scope, plus related tests and locale strings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread app/src/agentworld/pages/ProfilesSection.tsx Outdated
Comment thread app/src/agentworld/pages/ProfilesSection.tsx
@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

Synced onto current upstream/main (clean merge). The Frontend Checks failure was not in this PR's code: the only failing test was src/components/intelligence/__tests__/ObsidianVaultSection.test.tsx:120 — an Escape-closes-guide waitFor timeout in a component this PR does not touch (this PR only changes ProfilesSection + i18n). 379/380 test files passed; ProfilesSection's own tests were green. That is a flaky test unrelated to this change, so this is a re-run on top of current main.

pnpm typecheck + pnpm i18n:check clean (13 locales, 0 missing/extra).

@coderabbitai review

Re-run on current main; prior failure was an unrelated ObsidianVaultSection flake.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@M3gA-Mind Acknowledged. I’ll perform a fresh review of the rebased PR, focusing on the Profile editing changes and their test/i18n coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@graycyrus graycyrus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 17, 2026
…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.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

Thanks @graycyrus — both points addressed.

[major] Prefill race — FIXED. ProfileEditForm now tracks a per-field touched ref; the users.get prefill only applies to fields the user hasn't edited. If the fetch resolves after the user starts typing, their input is preserved instead of clobbered. Added a regression test that defers users.get, types into the display-name field before it resolves, then resolves it with a different record and asserts the typed value survives (fails before this change).

[major] update_profile authorization — confirmed NOT an IDOR. I traced the write path: the RPC handler (manifest.rs:400) does pass cryptoId through unchecked, but authorization is cryptographic at the backend, not trust-based at the RPC layer. The SDK's users.update_profile (vendor/tinyplace/sdk/rust/src/api/users.rs:35-45) signs the canonical user.profile payload — which is built from the target crypto_id via user_profile_signature_payload(crypto_id, &update) — with the caller's wallet signer and presents the signing key "so the backend can authorize the update." Since crypto_id is the wallet's Solana address, a caller can only produce a valid signature for the cryptoId whose private key they hold (their own). Passing someone else's cryptoId would sign with the caller's own key, and the backend rejects the mismatch. The local /rpc bridge is also token-gated to the app and uses the user's own wallet as signer, so there's no cross-user update path. So the description's "no security implications" holds for this frontend change.

Re: Closes vs follow-ups — N/A here; noting for the sibling directory PR #4990.

pnpm typecheck clean.

@coderabbitai review

@M3gA-Mind
M3gA-Mind requested a review from graycyrus July 17, 2026 15:02
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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/i18n

Length 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
done

Length 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 -v

Length 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)"
done

Length of output: 14710


@M3gA-Mind The delayed-prefill regression is fixed: the per-field touched guard preserves user input, and the new test exercises the late users.get resolution. I also verified the pinned Tinyplace SDK: update_profile signs a canonical payload containing the target cryptoId, so the reported authorization model rules out the previously suspected IDOR.

One remaining issue:

  • Major — allow an intentionally cleared avatar email to be saved. At Line 242, avatarEmail is omitted whenever the input is blank. After users.get prepopulates an existing Gravatar email, a user can delete it in the form, but Save omits the field and the backend preserves the old avatar email. Use touched.current.avatarEmail when building the patch: omit the field only if it was never edited, but submit avatarEmail: avatarEmail.trim() when the user has edited it, including '' to clear it. Please add a regression test that preloads an avatar email, clears the field, and asserts updateProfile receives avatarEmail: ''.

✏️ Learnings added
Learnt from: M3gA-Mind
URL: https://github.com/tinyhumansai/openhuman/pull/4996

Timestamp: 2026-07-17T15:03:14.569Z
Learning: For Tinyplace profile updates, `vendor/tinyplace` at commit `39ab493f51818ca940c3b80243c768ffcb90c744` signs the canonical `user.profile` payload with the target `cryptoId` and update fields through `UsersApi::update_profile`. Backend authorization is therefore cryptographically bound to the signer and target wallet, rather than trusted from OpenHuman's `handle_tinyplace_users_update_profile` RPC parameter alone.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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): the users.get prefill effect unconditionally calls setDisplayName/setBio/setAvatarEmail when 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 via directory.reverse can't edit. Confirm the fallback truly lacks a cryptoId. (= Codex @465.)
  • avatarEmail is omitted-when-empty, so it can never be cleared once set, while displayName/bio always 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.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

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 f53b99ed0, pushed 15:01, before this review). ProfileEditForm now tracks a per-field touched ref (ProfilesSection.tsx:206); the users.get prefill only applies to untouched fields (:216-221), and each onChange marks its field touched (:275/291/307). Regression test types before a deferred prefill resolves and asserts the edit survives.

Non-blocking — name can be blanked (asymmetric clearing): FIXED (this push). A display name is now required: handleSave guards against a blank name and the Save button is disabled while it's empty, with a translated agentWorld.profile.nameRequired message (added across all locales). bio may still be empty (optional); avatarEmail stays omitted-when-empty so an existing avatar isn't accidentally cleared. Regression test added.

Non-blocking — edit hidden on the directory-fallback path: confirmed the fallback does not lack a cryptoId — OwnedIdentity.cryptoId is populated from directory.reverse. The edit form is deliberately gated on the graphql profile source because that path is only reached when graphql.user returns null (i.e. no published profile yet — the UI prompts to register a handle first). Enabling edit there would mean creating a profile from the no-published-profile state, which is out of scope for this fill-the-gap PR; happy to file a follow-up if surfacing edit pre-publish is desired.

pnpm typecheck + pnpm i18n:check clean (13 locales, 0 missing/extra). Please re-review the current head.

@coderabbitai review

@M3gA-Mind
M3gA-Mind requested a review from oxoxDev July 17, 2026 16:30
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@M3gA-Mind Thanks for the detailed current-head context. I’ll re-review the updated PR, including the touched-field prefill guard, required display-name behavior and locale coverage, the intentional directory-fallback scope boundary, and the avatar-email update semantics.


✏️ Learnings added
Learnt from: M3gA-Mind
URL: https://github.com/tinyhumansai/openhuman/pull/4996

Timestamp: 2026-07-17T16:30:13.909Z
Learning: In app/src/agentworld/pages/ProfilesSection.tsx, editing is intentionally gated to the GraphQL profile source. The directory-fallback state occurs when graphql.user returns null and represents an unpublished profile that must register a handle first; supporting profile creation from that state is out of scope for PR `#4996`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. labels Jul 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/src/agentworld/pages/ProfilesSection.tsx (1)

211-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefill fetch only logs on failure.

The coding guideline requires verbose diagnostics covering entry/exit and external calls for new/changed flows. handleSave has entry/success/failure logs, but this prefill effect only logs the failure path — no entry or success log for the apiClient.users.get call.

♻️ 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')
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."
🤖 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 win

Extract 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb61376 and 3d917bb.

📒 Files selected for processing (16)
  • app/src/agentworld/pages/ProfilesSection.test.tsx
  • app/src/agentworld/pages/ProfilesSection.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts

Comment on lines +520 to +554
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();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +556 to +586
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'));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.”

@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

@graycyrus @oxoxDev — resolved.

[major] Prefill race (data loss) → FIXED (f53b99ed0, before your reviews). Per-field touched ref; users.get prefill applies only to untouched fields; each onChange marks touched. New regression test types before a deferred prefill resolves and asserts the edit survives (the existing tests waitFor prefill first, so they missed it — this one doesn't).

update_profile authorization (possible IDOR) → confirmed client-side + filed backend tracker #5024. I did not assume; I traced it:

  • The SDK signs the canonical user.profile payload, which includes cryptoId (vendor/tinyplace/sdk/rust/src/api/users.rs:124), with the caller's wallet signer, and presents the signer's public key (http.rs:412-428). Since cryptoId is the wallet's Solana address, a caller can only produce a valid signature for the cryptoId whose key they hold — signing for someone else's cryptoId presents a non-matching public key.
  • The core uses the local wallet as signer and /rpc is token-gated to the app; the UI only passes the user's own cryptoId. So there's no cross-user update path reachable from the client.
  • The one piece I can't verify from this repo is the tiny.place backend rejecting a signature whose key ≠ the target cryptoId owner (separate repo). Rather than assume it, I filed Verify backend enforces signer==cryptoId on user.profile updates (defense-in-depth, raised on #4996) #5024 to confirm that defense-in-depth, and updated the PR body's security line to describe the mechanism instead of a bald "no security implications."

Bonus — name can be blanked (oxoxDev, asymmetric clearing) → FIXED (3d917bb3f): display name required (guarded save + disabled button + agentWorld.profile.nameRequired in all locales + test).

pnpm typecheck + pnpm i18n:check clean (13 locales, 0 missing/extra). Please re-review.

@senamakel
senamakel merged commit 2114ad3 into tinyhumansai:main Jul 18, 2026
27 of 35 checks passed
senamakel pushed a commit to M3gA-Mind/openhuman that referenced this pull request Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tiny Place] Agent World profile editing (name/bio/avatar) is unreachable — updateProfile RPC has no UI

4 participants