feat(profiles): hermes-style agent profile homes — per-profile SOUL.md, workspace, memory, skills, cron attribution#5118
Conversation
…cated memory/workspace
Add per-profile "home" materialization and two new opt-in AgentProfile fields,
plus RPC path enrichment (spec sections A, B, C, E).
- home.rs (new): profile_home / profile_action_workspace path helpers,
validate_profile_id (^[a-z0-9][a-z0-9_-]{0,63}$, enforced only on new custom
ids so legacy ids keep loading), ensure_profile_home (idempotent seed of
personalities/<id>/SOUL.md + MEMORY.md, and <action_dir>/profiles/<id> when
dedicated_workspace), and dedicated_workspace_dir (the section-D seam).
- paths.rs: resolve_personality_soul now checks personalities/<id>/SOUL.md first
(re-read each prompt, hermes-style; skipped for ids that fail validation),
before the existing soul_md_path / inline / root fallbacks. New
effective_memory_suffix: legacy numeric memory_dir_suffix wins, else -<id> when
dedicated_memory (valid id), else shared "". PersonalityContext::from_profile
now derives its suffix through it.
- types.rs: add dedicated_memory + dedicated_workspace (serde default false,
camelCase over the wire); back-compat + round-trip tests.
- ops.rs: materialize the home on upsert (non-built-in) and select (covers
built-ins on first activation); list/select/upsert/delete now return payloads
enriched with read-only soulMdFile + workspaceDir resolved per profile.
- store.rs: expose normalise_profile_id so ops can locate the persisted profile.
Backward compatible: existing agent_profiles.json, numeric memory_dir_suffix, and
inline soul_md / soul_md_path all keep working (proven by extended tests). Home
seeding never overwrites a user's edited files.
…section D) When a session's active profile opts into dedicated_workspace, acting tools (shell/file/git) default their cwd to <action_dir>/profiles/<id> instead of the shared action_dir. Seam chosen: thread a tinyagents WorkspaceDescriptor into the top-level chat turn. investigation: acting tools already resolve cwd from ToolExecutionContext.workspace (a WorkspaceDescriptor) when present, else fall back to security.action_dir (tools/impl/system/shell.rs effective_action_dir_for_context). The top-level chat turn previously passed `None` for that descriptor at turn/graph.rs (subagents already use it for worktree isolation). So the smallest correct seam is to fill that existing slot: compute the descriptor in the session builder from profiles::dedicated_workspace_dir, carry it on Agent -> ChatTurnGraph -> run_turn_via_tinyagents_shared. Why not narrow SecurityPolicy.action_dir (the other candidate): that changes the agent's *write root*, i.e. a cross-profile write guard, which the spec defers to a follow-up. The descriptor sets only the default cwd; the profile dir lives under action_dir so SecurityPolicy already permits writes there and no hardening code is touched. `None` (the common shared case) is byte-for-byte unchanged. - factory.rs: build the descriptor (ensures the dir exists) and set it on the builder; + section-D seam unit tests. - session/types.rs + builder/setters.rs: Agent/AgentBuilder workspace_descriptor field + setter, defaulting to None. - turn/graph.rs + turn/core.rs: carry it onto ChatTurnGraph and pass it where the top-level turn used to hardcode None.
Update the Persona Pack capability description to cover multiple agent profiles with isolated identity (personalities/<id>/SOUL.md, re-read each message), optional dedicated memory subtree, and optional dedicated working directory. The profile home layout itself is documented in the profiles module doc comment (shipped with the domain commit).
Drive the profiles RPC surface end to end over the core HTTP router: upsert a custom profile with dedicatedMemory, assert the field round-trips and that list surfaces the resolved read-only soulMdFile (present only when the home was materialized on disk) with no workspaceDir for a memory-only profile, then select/delete and assert the active id falls back to default.
Add the profile home fields to the frontend: AgentProfile gains dedicatedMemory/dedicatedWorkspace (upsertable) and read-only soulMdFile/workspaceDir (resolved by the core, verified against enrich_profile in src/openhuman/profiles/ops.rs). ProfileEditorPage gets two toggles under a new "Isolation" section plus monospace read-only rows for the resolved paths when present. Adds Vitest coverage for the toggles' render/dispatch, edit-mode hydration of the read-only paths, and the api-layer field round-trip.
…ome guard, atomic seeds F1: extract the per-profile WorkspaceDescriptor expression into derive_profile_workspace_descriptor(), called by both build_session_agent_inner and its unit tests, so the tests can no longer drift from production. The descriptor's propagation to subagents is documented as deliberate profile isolation. F2: ensure_profile_home early-returns (with a [profiles] warn) when the id fails validate_profile_id, and enrich_profile skips soulMdFile/workspaceDir under the same check - keeping materialized homes and advertised paths symmetric with what the read paths (resolve_personality_soul/dedicated_workspace_dir/ effective_memory_suffix) will actually load. F3: enrich_profile warns (keeping the empty-map fallback) when a profile does not serialize to a JSON object instead of silently dropping every field. F4: SOUL.md/MEMORY.md first-time seeding is now atomic - write a temp file in the same dir then rename over the target (still only when absent), so a concurrent reader never observes a half-written seed. Idempotency semantics unchanged.
Establishes the two carriers a profile-scoped turn needs so later slices
(cross-profile write guard, experience scoping) can read "which profile is
active" without inventing a new channel:
- Tool layer: reuse the existing WorkspaceDescriptor.policy_id slot. The
session builder already stamps `openhuman.profile:<id>` on the dedicated-
workspace descriptor that reaches shell/file tools via ToolExecutionContext.
Centralize the encode/decode as `profiles::guard::{workspace_policy_id,
profile_id_from_policy_id}` so the builder and the (1b) tool gates can never
drift on the wire format; factory now uses the encoder.
- Hook/session layer: add `Agent.active_profile_id: Option<String>` (+ builder
field/setter), set from the resolved AgentProfile. This carries ANY active
profile (not just dedicated-workspace ones), which the (1c) experience
retrieval/capture keys on.
Seam choice: the WorkspaceDescriptor.policy_id already threads to both shell
(effective_action_dir_for_context) and file tools (security_for_tool_context),
so no new tool-boundary plumbing is needed for the guard. Considered a new
ToolExecutionContext field and a session task-local; both were redundant given
the descriptor already carries the identity, and a task-local would not survive
the subagent boundary the descriptor already crosses. TurnContext was considered
for the hook side but adding a field there churns ~35 struct literals for no
gain — the capture hook is built where the profile is in scope (1c), and the
retrieval side reads Agent.active_profile_id.
None-path is byte-identical: no profile -> active_profile_id None, policy_id
decodes to None. Pinned by builder tests (present/absent) + guard round-trip.
While a turn runs under a dedicated-workspace profile P, any tool
write/command whose resolved target lands in a sibling profile's workspace
`<action_dir>/profiles/<Q>/` (Q != P) is blocked with a `[policy-blocked]`
error naming both profiles. Mirrors hermes's classify_cross_profile_target.
Guard: `profiles::guard::classify_cross_profile_target(action_dir,
active_profile, target) -> Allow | Block{other_id}`. Symlink-safe — it
canonicalizes the profiles root and the target's deepest existing ancestor, so a
symlink inside P pointing at Q's dir, a `../Q/..` traversal, and a not-yet-
created write target all classify correctly.
Enforcement seams (single shared helper, two existing gate sites — this is why
the guard context is carried, not passed):
- File tools: `SecurityPolicy::{validate_path, validate_parent_path}` — the
resolved-path gate every file write tool funnels through. Armed via a new
optional `SecurityPolicy.active_profile` (ActiveProfileGuard{profile_id,
action_dir}), set once at session build by `with_active_profile` ONLY when the
active profile owns a dedicated workspace. It carries the broad action_dir
because `security_for_tool_context` overrides `action_dir` to the profile dir
per call, so the guard cannot re-derive the profiles root at check time.
- Shell: `scan_command_for_cross_profile` at the `run_with_security_in_context`
gate — shell never funnels through validate_path, so it scans path-shaped
command tokens against the profile's own cwd. Best-effort containment backstop
layered on the cwd (already the profile's dir); the airtight guarantee for
file mutations is the validate_path site.
Investigated alternatives: enforcing only in validate_path (misses shell);
threading the profile id through ToolExecutionContext per call (the
WorkspaceDescriptor.policy_id already carries it, but validate_path is a
SecurityPolicy method, so a policy field is the least-invasive carrier);
overriding WorkspaceDescriptor::allows in vendored tinyagents (too broad, edits
the SDK). Chose the session-scoped policy field + the two existing gate sites.
Prompt notice: `cross_profile_workspace_notice` renders one sentence naming the
profile's workspace and stating other profiles' dirs are off-limits, injected
via AgentProfilePromptSection (added even when the profile has no persona
suffix) — the hermes system-prompt disclosure.
Only TIGHTENS: `active_profile = None` (profile-less or shared-workspace
session) leaves validate_path/shell byte-identical; is_workspace_internal_path
and every other SecurityPolicy check are untouched. Pinned by the guard matrix
(same/other/outside/traversal/symlink/nonexistent), the shell-scan cases, the
validate_parent_path integration (block sibling / allow own / disarmed allows
sibling), and the prompt-notice render tests.
The agent_experience pool was global — one shared namespace and an unused agent_id retrieval filter. Partition it by the active profile so a profile only recalls its own operating lessons plus the shared/legacy pool, and never a sibling profile's. - Record: `AgentExperience.profile_id: Option<String>` (serde-defaulted; legacy stored records have none). Capture: `AgentExperienceCaptureHook::with_profile` stamps every captured record with the session's active profile (built from the resolved profile in the session builder); the profile-less session leaves it None. - Retrieval/injection + RPC: `ExperienceQuery.profile_id`, `RetrieveParams.profile_id`, and a new `ListParams.profile_id` all flow through the shared `experience_matches_profile` predicate. The turn's experience injection passes `Agent.active_profile_id` (1a) as the query profile. - Partition rule (documented + pinned): a profiled query sees records stamped with that profile PLUS unstamped legacy records, and excludes records stamped with a different profile. A profile-less query (None) sees EVERYTHING — the default session historically owns the whole shared pool and must keep recalling every record it and prior versions wrote, so narrowing it would silently drop guidance the default agent still relies on. None-path is byte-identical: no active profile -> records unstamped, queries recall the whole pool exactly as before. Pinned by the capture-stamp test, the retrieve/list partition matrix (P sees P+legacy not Q; None sees all), and the `experience_matches_profile` unit rules.
Add `<workspace>/personalities/<id>/skills/` as a per-profile skill
discovery root, surfaced ONLY for turns running under that profile.
Seam: the discovery/list layer, not the execution engine.
- `WorkflowScope::Profile` (highest collision precedence) marks bundles
found under a profile-local root; `discover_filtered` scans that root
last so a profile-local skill shadows a same-named global one for its
owner. New public entry points `discover_workflows_with_profile` /
`load_workflow_metadata_for_profile` take the root; the existing
`discover_workflows` / `load_workflow_metadata` delegate with `None`,
so the profile-less session and every other profile are byte-identical.
- `profiles::{profile_skills_dir, profile_skills_root}` resolve the root
(root gated on `validate_profile_id`); `ensure_profile_home` seeds the
empty `skills/` dir; `enrich_profile` advertises `skillsDir` when it
exists on disk.
- Harness catalog (factory `.workflows()`) + mid-session refresh
(`turn::tools::refresh_workflows`) + the `list_workflows` tool pass the
active profile's root; profile-local skills are implicitly allowed for
their owner (bypass the `allowed_skills` allowlist).
Alternatives considered: threading the root through describe/read/run
tools too, but those resolve via the global registry/skill_runtime
(`get_workflow`/`read_workflow_resource`/spawn) — the execution engine
the spec says not to fork. Left as follow-up; profile-local skills are
discoverable/listed/in-prompt but resolve for run/describe/read only if
also globally installed.
`skills` gate: new fns mirrored in `skills/stub.rs`; `all_tools_with_runtime`
gains a `profile_skills_root` param (unread when the feature is off).
Disabled build (`--no-default-features --features tokenjuice-treesitter`)
is green.
Tests: discovery scoping matrix (owner sees profile+global, not another
profile's; None sees neither), collision precedence (profile wins + tagged
Profile + resolves under the profile root), None==plain back-compat,
skills-dir creation, skillsDir enrichment.
Add an optional `profile_id` to cron jobs so a scheduled agent run can be
attributed to an agent profile and inherit its SOUL, memory scope,
dedicated-workspace descriptor, and tool/skill/MCP allowlists.
- `CronJob.profile_id: Option<String>` (`#[serde(default)]`) and
`CronJobPatch.profile_id: Option<Option<String>>` (None = no change,
Some(None) = clear), mirroring the existing `agent_id` shapes.
- Store: `add_column_if_missing("profile_id", "TEXT")` migration; the
column is threaded through the agent-job INSERT, all SELECT lists, the
row mapper, and the UPDATE patch. Legacy DBs/rows load with
`profile_id = None` — byte-identical.
- Run construction: `build_agent_for_cron_job` resolves the attributed
profile (`resolve_cron_profile`) and, when it still exists, builds the
run via `Agent::from_config_for_agent_with_profile` — the SAME
profile-aware path the task dispatcher uses — so the run is not a fork
of the execution engine. A deleted profile (or load error) warns and
falls through to the profile-less build; a profile-aware build error
also falls back. A job may still pin a built-in `agent_id`; otherwise
the profile's own `agent_id` is used.
- RPC: `cron.add` accepts `profile_id` (snake_case, matching the domain's
existing `agent_id`/`session_target` wire convention — NOT camelCase
`profileId`; the frontend cron client is snake_case too); `cron.update`
accepts it via `CronJobPatch`; `cron.list` returns it (serde). Schema
advertises the new input field.
Seed jobs (morning briefing, tinyplace autopilot, legacy welcome) pass
`None` — unattributed, unchanged.
Tests: schema back-compat (CronJob deserializes without the field;
CronJobPatch default None + explicit-None clearing), store round-trip
(create -> reload -> repoint -> clear -> untouched), shell-job has no
attribution, `resolve_cron_profile` present-vs-deleted fallback, and a
json_rpc_e2e round-trip (cron_add + cron_list + cron_update over /rpc
inside the profiles lifecycle test).
Deviation from spec wording: the spec said `profileId`; used snake_case
`profile_id` for consistency with the cron domain's existing wire fields.
- profiles/mod.rs home-layout doc: add the `personalities/<id>/skills/` row (owner-only skill discovery, implicitly allowed, wins same-name collisions, surfaced as `skillsDir`) and a "Cron attribution" section describing `CronJob.profile_id` and the deleted-profile fallback. - about_app catalog (Persona Pack): mention private per-profile skills and cron-job profile attribution in the capability description.
…a follow-up) Close the "listed but unrunnable" gap from 2a: a profile-local skill that appears in list_workflows / the prompt catalog can now also be described, read, and run by the session that owns it — same precedence and owner-only visibility as discovery. Seam: extend the existing resolution functions with `_with_profile` variants that thread the profile skills root down to `discover_*`, and keep the old signatures delegating with `None` (byte-identical for the profile-less session and other profiles). No global registry state is mutated, so concurrent sessions under different profiles never see each other's private skills. - registry: `load_workflows_with_profile` / `get_workflow_with_profile` (used by describe + run) scan the profile root via `discover_workflows_with_profile`; `load_workflows` / `get_workflow` delegate with None. - ops_discover: `read_workflow_resource_with_profile` (used by read) resolves via `load_workflow_metadata_for_profile`; `read_workflow_resource` delegates with None. New `profile_local_skill_ids` returns the owner's private skill ids for the implicit-allow check. - skill_runtime: `spawn_workflow_run_background_with_profile` passes the root (owned, crosses the spawn boundary) into `get_workflow_with_profile`; the old fn delegates with None. - tools: `WorkflowDescribeTool` / `WorkflowReadResourceTool` / `RunWorkflowTool` gain `with_profile_skills_root`, wired from `all_tools_with_runtime`'s existing `profile_skills_root` channel (the same one 2a added for the list tool). Profile-local skills are implicitly allowed for their owner (bypass `allowed_skills`), consistent with `list_workflows`. Precedence: profile-local wins same-name collisions for the owner via the existing `WorkflowScope::Profile` ordering — the resolver already dedups by that precedence, so describe/read/run resolve the profile-local copy while everyone else resolves the global one. skills gate: all new code lives inside `#[cfg(feature = "skills")]` modules (registry/ops_discover/run_machinery/tools) whose callers are gated by the same feature, so no stub changes are needed; the disabled build (`--no-default-features --features tokenjuice-treesitter`) stays green. Tests: `get_workflow_with_profile` matrix (describe + run resolution) and `read_workflow_resource_with_profile` matrix (read) each pin owner-resolves, collision-resolves-to-profile-local, other-profile/profile-less cannot resolve, and global-only resolves everywhere; plus `profile_local_skill_ids` lists only the profile root. Skills + skill_runtime suites: 186 passed.
Add a read-only skills directory path row (with a hint that SKILL.md workflows there are private to the profile) to ProfileEditorPage, backed by the enriched `skillsDir` field on AgentProfile. Includes the new en.ts strings and real translations across all 13 locales for both the skills-directory rows and the cron attribution picker (wired next).
Add an agent-profile picker to the cron create/edit form (agent jobs only) wired to the snake_case `profile_id` wire field: create sends the id when set and omits it for "no profile"; edit sends the id or `null` to clear (double-option contract). A deleted-but-still-attributed profile stays selectable by its raw id. The job list shows the resolved profile name, falling back to the raw id when the profile is gone. CronJobsPanel loads profiles from the agentProfile slice.
…elds
`CronJobPatch.profile_id: Option<Option<String>>` with plain serde
deserializes a wire `null` identically to an absent key (both collapse to
the outer `None`), so the UI's "clear attribution" patch
(`{"profile_id": null}`) was a silent no-op — the job kept its profile.
Fix: a `deserialize_double_option` helper (used via `#[serde(default,
deserialize_with = ...)]`) restores real double-option semantics —
absent key -> None (no change), present null -> Some(None) (clear),
present value -> Some(Some(v)) (set). serde only invokes a
`deserialize_with` when the key is present, which is what makes the
absent-vs-null distinction recoverable.
Applied consistently to BOTH `profile_id` and `agent_id`: `agent_id` is the
same `Option<Option<String>>` shape and shared the identical latent bug, and
its struct doc + the existing `cron_job_patch_agent_id_supports_explicit_none_clearing`
test already document the `Some(None)` = clear intent. Fixing it is
behavior-neutral for real callers (the frontend never patches `agent_id` — it
is a create-only param; the patch TS type omits it) and aligns the wire with
the documented struct semantics, so it is the "trivially safe + clearly
intended" case rather than a silent behavior change. No other `CronJobPatch`
field is a clearable `Option<Option<_>>`; the remaining fields are plain
`Option<T>` where absent and null both correctly mean "no change".
The codebase already had this exact helper as `deserialize_nullable_patch`
in `app_state/schemas.rs` (private to that domain); this adds a local,
slightly simpler equivalent to keep the cron domain self-contained.
Tests: `patch_profile_id_wire_double_option_semantics` /
`patch_agent_id_wire_double_option_semantics` pin absent/null/value through
the real `serde_json::from_value` path (the same one `read_required::<CronJobPatch>`
uses); the json_rpc_e2e cron round-trip now updates a job with
`profile_id: null` over `/rpc` and asserts `cron_list` shows the attribution
gone. cargo check green; cron types+store lib tests 47 passed; e2e passed.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds profile-scoped homes, memory, workspaces, skills, experiences, security enforcement, cron attribution, enriched RPC responses, and corresponding settings UI, localization, compatibility tests, and end-to-end coverage. ChangesProfile-scoped agent execution
Cron attribution and UI
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 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: 21e4683a41
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/openhuman/profiles/store.rs (1)
148-186: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve or disable the isolation toggles for the default profile The editor exposes
dedicatedMemoryanddedicatedWorkspacefor the master profile, but this merge path resets both tofalseforDEFAULT_PROFILE_IDwhile other profiles keep the submitted values. That makes the toggles appear to save and then silently disappear; either carry them through here or hide/disable them for the default profile.🤖 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 `@src/openhuman/profiles/store.rs` around lines 148 - 186, The DEFAULT_PROFILE_ID merge in upsert must preserve the submitted dedicatedMemory and dedicatedWorkspace isolation settings instead of resetting them through built_in_default_profile(). Carry both fields from profile into default, or disable their editor controls for the default profile so unsupportable values cannot be submitted.src/openhuman/skill_runtime/run_machinery.rs (1)
144-181: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftScope workflow run logs by profile
run_log_pathstores logs under a workspace-wide.runs/directory, andlist_workflow_runs/read_workflow_run_logread from that shared store without any owner filter. That lets a caller who knows or guesses a workflow/run id read another profile’s private skill prompt, inputs, and output. Include the owning profile in the path or filter lookups against the caller’s allowed skill set.🤖 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 `@src/openhuman/skill_runtime/run_machinery.rs` around lines 144 - 181, Scope workflow run log storage and retrieval by the owning profile or the caller’s allowed skill set. Update run_log_path usage in the workflow launch flow and the list_workflow_runs/read_workflow_run_log lookup paths so logs cannot be shared across profiles, while preserving existing run creation and reading behavior for authorized workflows.
🧹 Nitpick comments (3)
src/openhuman/agent_experience/store.rs (1)
130-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a debug log for the new profile-partition branch.
experience_matches_profilefiltering inretrieve/list_for_profilehas no diagnostics (unlike the analogous scoping inskills/tools.rs::executeor theprofile_skills_rootactivation log infactory.rs). Alog::debug!with before/after counts and the activeprofile_idwould make "why isn't my profile seeing this experience" easy to grep for.As per path instructions,
**/*.rsshould "use verbose, grep-friendly diagnostics for new or changed flows, including entry/exit, branches, ... and errors."Also applies to: 168-205
🤖 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 `@src/openhuman/agent_experience/store.rs` around lines 130 - 147, Add grep-friendly log::debug diagnostics around experience_matches_profile filtering in list_for_profile and retrieve, including the active profile_id and before/after result counts. Preserve the existing filtering behavior and log the scoped flow clearly enough to diagnose missing profile experiences.Source: Path instructions
src/openhuman/skills/tools.rs (2)
68-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing unit coverage for the new "profile-local skills bypass
allowed_skills" predicate.Both sites introduce the same new security-relevant rule (a profile's own private skills implicitly pass the allowlist check) but neither has an inline test, even though the predicate itself is pure logic (no Config/Agent/filesystem live dependency beyond
profile_local_skill_ids, which theops_discover.rstests already show is easy to seed withtempfile::TempDir). Given this rule directly governs cross-profile skill isolation, a couple of focused unit tests would give solid regression protection cheaply.
src/openhuman/skills/tools.rs#L68-L79: add a#[cfg(test)] mod testscoveringskill_allowed_including_profilefor the four combinations of {allowlist present/absent} × {id in profile_local/not}.src/openhuman/agent/tools/run_workflow.rs#L324-L336: add a test (or reuse a shared test helper) assertingRunWorkflowTool::executepermits a profile-local id even when it's absent fromskill_allowlist, and still rejects a non-local, non-allowlisted id.🤖 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 `@src/openhuman/skills/tools.rs` around lines 68 - 79, Add focused unit coverage for the profile-local allowlist bypass. In src/openhuman/skills/tools.rs#L68-79, add tests for skill_allowed_including_profile covering allowlist present/absent and skill ID local/non-local combinations. In src/openhuman/agent/tools/run_workflow.rs#L324-336, add or reuse a test helper to verify RunWorkflowTool::execute permits a profile-local ID absent from skill_allowlist while rejecting a non-local, non-allowlisted ID.Source: Coding guidelines
221-236: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRedundant profile-skills-root scan when no allowlist is configured.
profile_local_skill_ids(self.profile_skills_root.as_deref())is computed unconditionally in bothWorkflowDescribeTool::executeandWorkflowReadResourceTool::execute, even whenself.skill_allowlistisNone(in which caseskill_allowed_including_profilewill returntrueanyway, sinceskill_allowed(None, _)is permissive). This does a directory scan + frontmatter parse of the profile's skills root on every call for that (common) case.RunWorkflowTool::executeavoids this by gating the equivalent scan behindif let Some(allow) = &self.skill_allowlist.♻️ Proposed fix — gate the scan behind an active allowlist
let skill_id = read_workflow_id(&args)?; - let profile_local = profile_local_skill_ids(self.profile_skills_root.as_deref()); - if !skill_allowed_including_profile(&self.skill_allowlist, &profile_local, &skill_id) { - log::debug!("[profiles] describe_workflow blocked by profile allowlist: {skill_id}"); - return Ok(ToolResult::error(format!( - "describe_workflow: workflow `{skill_id}` is not available to the active agent profile" - ))); + if self.skill_allowlist.is_some() { + let profile_local = profile_local_skill_ids(self.profile_skills_root.as_deref()); + if !skill_allowed_including_profile(&self.skill_allowlist, &profile_local, &skill_id) { + log::debug!("[profiles] describe_workflow blocked by profile allowlist: {skill_id}"); + return Ok(ToolResult::error(format!( + "describe_workflow: workflow `{skill_id}` is not available to the active agent profile" + ))); + } }(apply the analogous change to
WorkflowReadResourceTool::execute)Also applies to: 306-325
🤖 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 `@src/openhuman/skills/tools.rs` around lines 221 - 236, Gate the profile-local skill ID scan in WorkflowDescribeTool::execute and WorkflowReadResourceTool::execute behind the presence of self.skill_allowlist, reusing the allowlist branch pattern from RunWorkflowTool::execute. When no allowlist is configured, skip profile_local_skill_ids entirely while preserving the existing permissive authorization behavior; when configured, continue passing the scanned IDs to skill_allowed_including_profile.
🤖 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/components/settings/panels/cron/CronJobFormModal.tsx`:
- Around line 589-612: Associate the profile label with the select in the
agent-only form section by adding a matching id to the profile select and
htmlFor to the label. Update the label and select elements near the profileId
binding, preserving the existing control behavior and styling.
In `@app/src/lib/i18n/es.ts`:
- Around line 7442-7452: Update the settings.profiles.editor.skillsDirHint
Spanish translation to describe SKILL.md entries as habilidades or archivos
SKILL.md rather than flujos, while preserving the existing meaning that they are
private to the profile.
In `@app/src/lib/i18n/id.ts`:
- Around line 7328-7332: Update the Indonesian translations for
settings.profiles.editor.soulMdFile and settings.profiles.editor.skillsDirHint
to explicitly preserve the SOUL.md and SKILL.md file terminology. Replace the
generic identity-file wording and “Alur kerja” phrasing with wording that
identifies the SOUL.md file and profile-local SKILL.md files as files, while
leaving the surrounding translation keys unchanged.
In `@app/src/lib/i18n/pl.ts`:
- Around line 7408-7410: Update the `settings.profiles.editor.skillsDirHint`
Polish translation to refer to the SKILL.md files as files or skills rather than
workflows, while preserving the meaning that they are private to the profile.
In `@app/src/lib/i18n/ru.ts`:
- Line 7379: Update the `settings.profiles.editor.soulMdFile` Russian
translation to retain the literal `SOUL.md` filename in the label, while
preserving the existing meaning and localization.
In `@docs/TEST-COVERAGE-MATRIX.md`:
- Around line 632-638: Reconcile the status totals in the coverage summary
table: update the affected count or the “Total leaves” explicit value so
Covered, Partial, Missing, and Manual smoke sum consistently with the reported
total, while preserving the nested product-feature count unless it also depends
on the correction.
In `@src/openhuman/agent/harness/session/builder/factory.rs`:
- Around line 1521-1560: Update derive_profile_workspace_descriptor so a
create_dir_all failure returns None after logging the warning, allowing callers
to fall back to the shared action_dir instead of constructing a descriptor for a
nonexistent directory. Preserve the existing Some descriptor path when directory
creation succeeds, and add a regression test covering creation failure if the
surrounding test structure supports it.
In `@src/openhuman/agent/harness/session/turn/tools.rs`:
- Around line 329-339: In the mid-session refresh around active_profile_id and
load_workflow_metadata_for_profile, add a namespaced diagnostic that identifies
whether profile-aware discovery was used or shared discovery was selected, and
reports the number of loaded metadata entries. Do not include the profile ID or
resolved skills path; ensure invalid profile IDs are represented by the
shared-discovery branch.
- Around line 329-339: Add a refresh_workflows test that configures an
active_profile_id, creates a workflow under the corresponding
profile_skills_root(...) directory at <workspace>/personalities/<id>/skills/,
and verifies the workflow is discovered during refresh. Keep the existing
profile-less test unchanged and cover the profile-specific discovery branch.
In `@src/openhuman/cron/schemas.rs`:
- Around line 321-326: The new profile-attribution flow lacks privacy-safe
diagnostics. In src/openhuman/cron/schemas.rs lines 321-326, add structured
tracing after parsing profile_id and around the agent-job creation call, logging
only has_profile_attribution and never the ID; in
app/src/components/settings/panels/cron/CronJobFormModal.tsx lines 232-234 and
257-259, add hasProfileAttribution to the create-submit and update-submit
metadata respectively.
In `@src/openhuman/profiles/guard.rs`:
- Around line 137-169: Update scan_command_for_cross_profile to fail closed for
shell-expanded tokens: before the path-shaped check, detect tokens containing
`$` or backtick substitution syntax and return a blocking result for an
unresolvable target, using the function’s existing Option<String> contract and
an appropriate profile identifier. Preserve normal expansion and classification
for statically resolvable path tokens.
---
Outside diff comments:
In `@src/openhuman/profiles/store.rs`:
- Around line 148-186: The DEFAULT_PROFILE_ID merge in upsert must preserve the
submitted dedicatedMemory and dedicatedWorkspace isolation settings instead of
resetting them through built_in_default_profile(). Carry both fields from
profile into default, or disable their editor controls for the default profile
so unsupportable values cannot be submitted.
In `@src/openhuman/skill_runtime/run_machinery.rs`:
- Around line 144-181: Scope workflow run log storage and retrieval by the
owning profile or the caller’s allowed skill set. Update run_log_path usage in
the workflow launch flow and the list_workflow_runs/read_workflow_run_log lookup
paths so logs cannot be shared across profiles, while preserving existing run
creation and reading behavior for authorized workflows.
---
Nitpick comments:
In `@src/openhuman/agent_experience/store.rs`:
- Around line 130-147: Add grep-friendly log::debug diagnostics around
experience_matches_profile filtering in list_for_profile and retrieve, including
the active profile_id and before/after result counts. Preserve the existing
filtering behavior and log the scoped flow clearly enough to diagnose missing
profile experiences.
In `@src/openhuman/skills/tools.rs`:
- Around line 68-79: Add focused unit coverage for the profile-local allowlist
bypass. In src/openhuman/skills/tools.rs#L68-79, add tests for
skill_allowed_including_profile covering allowlist present/absent and skill ID
local/non-local combinations. In
src/openhuman/agent/tools/run_workflow.rs#L324-336, add or reuse a test helper
to verify RunWorkflowTool::execute permits a profile-local ID absent from
skill_allowlist while rejecting a non-local, non-allowlisted ID.
- Around line 221-236: Gate the profile-local skill ID scan in
WorkflowDescribeTool::execute and WorkflowReadResourceTool::execute behind the
presence of self.skill_allowlist, reusing the allowlist branch pattern from
RunWorkflowTool::execute. When no allowlist is configured, skip
profile_local_skill_ids entirely while preserving the existing permissive
authorization behavior; when configured, continue passing the scanned IDs to
skill_allowed_including_profile.
🪄 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: 91bb1160-2853-410c-9803-0d011bdd2e52
📒 Files selected for processing (80)
app/src/components/settings/panels/CronJobsPanel.test.tsxapp/src/components/settings/panels/CronJobsPanel.tsxapp/src/components/settings/panels/ProfileEditorPage.test.tsxapp/src/components/settings/panels/ProfileEditorPage.tsxapp/src/components/settings/panels/cron/CoreJobList.test.tsxapp/src/components/settings/panels/cron/CoreJobList.tsxapp/src/components/settings/panels/cron/CronJobFormModal.test.tsxapp/src/components/settings/panels/cron/CronJobFormModal.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.tsapp/src/services/api/agentProfilesApi.test.tsapp/src/types/agentProfile.tsapp/src/utils/tauriCommands/cron.tsdocs/TEST-COVERAGE-MATRIX.mdsrc/openhuman/about_app/catalog_data.rssrc/openhuman/agent/harness/session/builder/builder_tests.rssrc/openhuman/agent/harness/session/builder/factory.rssrc/openhuman/agent/harness/session/builder/setters.rssrc/openhuman/agent/harness/session/turn/core.rssrc/openhuman/agent/harness/session/turn/graph.rssrc/openhuman/agent/harness/session/turn/tools.rssrc/openhuman/agent/harness/session/types.rssrc/openhuman/agent/tools/run_workflow.rssrc/openhuman/agent_experience/capture.rssrc/openhuman/agent_experience/ops.rssrc/openhuman/agent_experience/prompt.rssrc/openhuman/agent_experience/schemas.rssrc/openhuman/agent_experience/store.rssrc/openhuman/agent_experience/types.rssrc/openhuman/channels/runtime/startup.rssrc/openhuman/cron/scheduler.rssrc/openhuman/cron/scheduler_tests.rssrc/openhuman/cron/schemas.rssrc/openhuman/cron/seed.rssrc/openhuman/cron/store.rssrc/openhuman/cron/store_tests.rssrc/openhuman/cron/types.rssrc/openhuman/profiles/guard.rssrc/openhuman/profiles/home.rssrc/openhuman/profiles/mod.rssrc/openhuman/profiles/ops.rssrc/openhuman/profiles/paths.rssrc/openhuman/profiles/prompt_section.rssrc/openhuman/profiles/schemas.rssrc/openhuman/profiles/store.rssrc/openhuman/profiles/types.rssrc/openhuman/runtime_node/ops.rssrc/openhuman/security/policy/enforcement.rssrc/openhuman/security/policy/mod.rssrc/openhuman/security/policy/path_checks.rssrc/openhuman/security/policy/policy_tests.rssrc/openhuman/security/policy/types.rssrc/openhuman/skill_runtime/mod.rssrc/openhuman/skill_runtime/run_machinery.rssrc/openhuman/skills/ops.rssrc/openhuman/skills/ops_create.rssrc/openhuman/skills/ops_discover.rssrc/openhuman/skills/ops_install.rssrc/openhuman/skills/ops_tests.rssrc/openhuman/skills/ops_types.rssrc/openhuman/skills/registry.rssrc/openhuman/skills/stub.rssrc/openhuman/skills/tools.rssrc/openhuman/tools/impl/system/shell.rssrc/openhuman/tools/ops.rstests/json_rpc_e2e.rstests/personality_e2e.rstests/raw_coverage/inference_agent_raw_coverage_e2e.rs
…numeric suffix Two review fixes to the profiles domain (PR tinyhumansai#5118 review). 1. Preserve edited SOUL.md from Settings (Codex, home.rs). ensure_profile_home only seeds SOUL.md when absent, and resolve_personality_soul reads the file first, so a persona edited in Settings after the home already existed updated agent_profiles.json but never reached the prompt. Add sync_soul_md_on_upsert(): on an explicit (non-built-in) upsert, when profile.soul_md is Some(non-empty) and differs from the on-disk SOUL.md, atomically overwrite the file (same temp+rename as the seed path). Empty/None soul_md leaves the file untouched so a manual file edit stays authoritative; select never calls it, so it can't clobber a manual edit with a stale inline value. Wired into ops::upsert (non-fatal). 2. Let dedicated_memory override the auto-assigned numeric suffix (Codex, paths.rs). AgentProfileStore::upsert stamps every non-default profile with Some("-1"), Some("-2"), … so effective_memory_suffix's "legacy numeric wins" branch made the dedicated_memory toggle dead code (always memory-1, never memory-<id>). Flip precedence: dedicated_memory (with a valid id) wins; the numeric suffix applies only when dedicated_memory is off (back-compat for existing non-dedicated profiles). Back-compat note: toggling dedicated_memory ON is an explicit user opt-in to a fresh dedicated subtree, so switching the subtree on toggle is intended. Doc comments (paths.rs, mod.rs) updated. Tests: sync overwrite/no-op/invalid-id cases; flipped-precedence matrix (dedicated-wins-over-numeric, numeric-retained-when-not-dedicated, invalid-id-dedicated-falls-back-to-numeric).
The storage key (id) was derived in build_experience before profile_id was stamped in on_turn_complete, so identical task/tool/outcome triples from different profiles collapsed onto one experience/<id> key and the later store.put() overwrote the earlier profile's record (Codex, capture.rs). Add stable_experience_id_for_profile(): mixes the profile id into the digest only when Some(non-empty); None is byte-identical to the pre-1c derivation so existing stored (legacy/profile-less) records keep their identity. Re-derive the id in on_turn_complete after stamping the profile. Tests: None matches the legacy derivation (incl. blank id); A/B/None yield three distinct keys; hook-level test proves three records coexist instead of one being overwritten, and the None record's key equals the legacy derivation.
…an't be created derive_profile_workspace_descriptor logged a create_dir_all failure but still returned Some(descriptor), binding every acting tool (shell/file/git) to a nonexistent cwd and silently breaking the whole profile session (CodeRabbit, factory.rs). Return None on create failure so callers fall back to the shared action_dir cwd, matching the docstring's "common case" fallback. Doc updated; regression test points action_dir at a regular file so profiles/… can't be created and asserts None.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d5ec42142
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 995f09a63c
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4ba7c5458
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # tests/raw_coverage/composio_credentials_state_raw_coverage_e2e.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad4df38ade
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
openhuman/src/openhuman/agent/harness/session/builder/factory.rs
Lines 843 to 848 in 7cc90a4
When a profile restricts composioIntegrations and the Composio cache is cold, this only filters the prewarmed snapshot captured during construction; the first turn later calls fetch_connected_integrations() and refresh_delegation_tools() with no profile allowlist, so the session can repopulate all connected toolkits and expose off-profile connectors in prompts/tools. The same widening can happen on later cache refreshes or subagent refreshes, so carry the profile connector allowlist on the agent/parent context and apply filter_integrations after every live or cached Composio integration fetch.
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29b5b46c26
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b25d03439
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let guard_command = inline_code.clone().unwrap_or_else(|| { | ||
| std::iter::once(script_path.as_deref().unwrap_or_default()) | ||
| .chain(extra_args.iter().map(String::as_str)) | ||
| .collect::<Vec<_>>() | ||
| .join(" ") | ||
| }); |
There was a problem hiding this comment.
Classify resolved script paths before spawning Node
When node_exec is used from a profiled workspace with a top-level symlink such as link.js -> ../bob/evil.js, this guard only scans the raw script_path token (link.js), which is not path-shaped and is skipped by the cross-profile scanner. resolve_script_path later just joins it under Alice's cwd and Node follows the symlink, so the script can execute from Bob's profile (and write via __dirname) despite the active profile guard; classify the resolved/canonical script path or reject symlinked script targets before spawning.
Useful? React with 👍 / 👎.
| let mut agent = match Agent::from_config_for_agent_with_profile( | ||
| &config, | ||
| "orchestrator", | ||
| None, | ||
| active_profile | ||
| .as_ref() | ||
| .and_then(|profile| profile.system_prompt_suffix.clone()), | ||
| active_profile.as_ref(), |
There was a problem hiding this comment.
Apply profile model overrides to workflow agents
For a profile that sets modelOverride, workflows launched through run_workflow are still built from the freshly loaded config here without overlaying the profile model first. from_config_for_agent_with_profile uses the profile for SOUL/tools and reads profile temperature, but it does not apply profile.model_override (web chat and cron do that by mutating the effective config before calling it), so profile-attributed workflow runs silently use the default chat model instead of the selected profile model.
Useful? React with 👍 / 👎.
| ); | ||
| return None; | ||
| }; | ||
| let sub_paths = discover_subagent_files(raw_dir, &root_stem); |
There was a problem hiding this comment.
Persist profiled sub-agent transcripts beside the root
For dedicated-memory/profile-scoped sessions, the root transcript now lives under session_raw-<profile> and this resolver only looks for sub-agent siblings in that same directory. However the sub-agent graph still persists child transcripts via resolve_keyed_transcript_path(workspace_dir, ...), which writes to the shared session_raw/, so profiled threads that delegate work lose their sub-agent trail in the transcript view/cache even though the files were written; thread the session raw subdir into sub-agent persistence or keep discovery scanning the legacy shared location for matching stems.
Useful? React with 👍 / 👎.
Summary
<workspace>/personalities/<id>/with a file-backedSOUL.md(hot-read on every prompt build), a seededMEMORY.md, and a privateskills/directory.dedicatedMemory(ownmemory-<id>/memory_tree-<id>/session_raw-<id>subtree, replacing the numeric-suffix scheme, which stays supported) anddedicatedWorkspace(<action_dir>/profiles/<id>/becomes the run's default tool cwd via the existing tinyagentsWorkspaceDescriptorseam).file_safetyequivalent): a session running as profile P cannot write into another profile's workspace, enforced at both the file-tool path gate and the shell command scanner, with a system-prompt notice.SKILL.mdworkflows under a profile'sskills/dir are discoverable, listed, describable, readable, and runnable only for their owner profile; profile-local wins id collisions for the owner.profile_id; scheduled runs execute with that profile's soul/memory/workspace/allowlists, with graceful fallback when the profile was deleted (incl. proper wirenullclearing semantics).soulMdFile/workspaceDir/skillsDirpaths), cron job profile picker with deleted-profile fallback, full i18n in all 14 locales.Problem
OpenHuman profiles ("personalities") were prompt-level only: a suffix, allowlists, and a memory-dir suffix hack layered on a shared workspace and a shared memory/experience pool. Running multiple agents concurrently meant they shared a cwd, could write into each other's state, shared one procedural-experience pool, and had no per-agent identity file a user could edit on disk. The hermes-agent model (NousResearch/hermes-agent) — profile = directory with its own identity file, workspace, and memories — is the reference; this PR closes the gap so multiple isolated agents can run at the same time.
Solution
profiles/home.rsmaterializes homes idempotently (atomic temp+rename seeds, never overwrites user edits);validate_profile_id(hermes-style slug rule) gates all path construction, and ids failing it are skipped consistently on both the read and materialization paths (stored ids are additionally slugified on load).action_dir(notworkspace_dir) because workspace internals are agent-tool-denied fail-closed; no security hardening was loosened. The workspace descriptor fills the previously-Nonetop-level chat-turn slot; propagation to spawned subagents is intentional profile isolation.WorkspaceDescriptor.policy_id(openhuman.profile:<id>) at the tool layer,Agent.active_profile_idat the harness layer.profiles/guard.rs) called from the existingSecurityPolicy::validate_path/validate_parent_pathgates and a shell-command scanner; it only tightens — profile-less sessions are byte-identical to before.*_with_profilevariants); no global registry state is mutated, so concurrent sessions under different profiles cannot cross-contaminate. Compiles green with theskillsfeature gate off.null= clear, value = set), matching the documented intent and the existingapp_statepattern.Submission Checklist
docs/TEST-COVERAGE-MATRIX.md## RelatedN/A: no release-cut surface changed (settings panels and core RPC only)Closes #NNN—N/A: no tracking issue exists for this featureImpact
agent_profiles.jsonpayloads, numericmemory_dir_suffixprofiles, inlinesoul_md/soul_md_path, and cron jobs withoutprofile_idall keep working (serde defaults + tests).--no-default-features --features tokenjuice-treesitter(skills gate off) compiles clean; new skills code is inside#[cfg(feature = "skills")].is_workspace_internal_pathand all existing SecurityPolicy checks are untouched.--no-verifybecause the pre-push hook'scargo clippy -D warningsfails on pre-existing upstream/main code (src/openhuman/proc_metrics/mod.rs:216,clippy::unnecessary_map_on_constructor, introduced in feat(dev): library-mode benchmark environment, fleet/budget gates, and profiling optimizations #5107; this branch does not touch that file). TypeScript and cmd-token checks pass.Related
proc_metrics/mod.rs:216Summary by CodeRabbit