Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
0bd6345
feat(profiles): hermes-style profile homes, file-backed SOUL.md, dedi…
senamakel Jul 22, 2026
cf2d29f
feat(profiles): per-profile dedicated workspace as default tool cwd (…
senamakel Jul 22, 2026
6f8c787
docs(profiles): describe multi-profile homes in the about_app catalog
senamakel Jul 22, 2026
71967be
test(profiles): json-rpc e2e for dedicated-memory profile lifecycle
senamakel Jul 22, 2026
7b6b25f
feat(profiles-ui): dedicated memory/workspace toggles + resolved paths
senamakel Jul 22, 2026
8c74c7a
feat(profiles-ui): i18n strings for dedicated memory/workspace toggles
senamakel Jul 22, 2026
b140208
fix(profiles): review fixes - real descriptor-seam test, invalid-id h…
senamakel Jul 22, 2026
3d6a2a2
feat(profiles): plumb active profile id to the tool + hook layers (1a)
senamakel Jul 22, 2026
8821c9a
feat(profiles): cross-profile write guard + prompt notice (1b)
senamakel Jul 22, 2026
e671d99
feat(agent_experience): scope procedural experience by profile (1c)
senamakel Jul 22, 2026
3a32bf4
feat(profiles): per-profile skills directory (2a)
senamakel Jul 22, 2026
ec2831a
feat(cron): per-profile cron job attribution (2b)
senamakel Jul 22, 2026
4bdb96a
docs(profiles): document per-profile skills dir + cron attribution (2c)
senamakel Jul 22, 2026
9ddbb84
feat(profiles): resolve profile-local skills for describe/read/run (2…
senamakel Jul 22, 2026
ceaadca
feat(profiles-ui): surface per-profile skills directory + i18n
senamakel Jul 22, 2026
233c160
feat(profiles-ui): per-profile cron attribution picker
senamakel Jul 22, 2026
981b7a5
fix(cron): honor wire null as clear for CronJobPatch double-option fi…
senamakel Jul 22, 2026
273a93a
docs(profiles): add agent profile homes section to test coverage matrix
senamakel Jul 22, 2026
21e4683
style: cargo fmt on agent_experience + policy tests
senamakel Jul 22, 2026
d0e72dd
fix(profiles): sync edited SOUL.md on upsert; dedicated_memory beats …
senamakel Jul 22, 2026
9e411b2
fix(agent_experience): partition captured experience keys by profile
senamakel Jul 22, 2026
f5ca3dc
fix(profiles): fall back to shared cwd when dedicated workspace dir c…
senamakel Jul 22, 2026
3dd8ef1
fix(profiles): harden shell cross-profile scan + document its guarant…
senamakel Jul 22, 2026
1d3f7da
chore(profiles): privacy-safe diagnostics for skills refresh + cron a…
senamakel Jul 22, 2026
109e775
fix(profiles-ui): associate cron profile label; correct SKILL.md/SOUL…
senamakel Jul 22, 2026
7b9d07e
docs: reconcile coverage-matrix totals with the real status counts
senamakel Jul 22, 2026
20bc437
style(i18n): prettier formatting on ko locale
senamakel Jul 22, 2026
4a71e54
fix(profiles): propagate dedicated-workspace descriptor to subagents …
senamakel Jul 22, 2026
5b6473b
fix(profiles): sync SOUL.md on upsert for built-in profiles too
senamakel Jul 22, 2026
9d385fe
fix(profiles): apply dedicated memory + profile SOUL.md on the ordina…
senamakel Jul 22, 2026
c1b7192
Merge remote-tracking branch 'refs/remotes/upstream/main' into pr/5118
senamakel Jul 22, 2026
814bd4b
fix(security): protect profile homes as internal state
senamakel Jul 22, 2026
b8751db
fix(skills): deduplicate profile workflows by runnable id
senamakel Jul 22, 2026
9e0f693
fix(profiles): preserve isolation across stores and workflow runs
senamakel Jul 22, 2026
bdc4097
fix(skills): keep disabled build warning-free
senamakel Jul 22, 2026
cc5ddfe
fix(profiles): satisfy transcript path clippy
senamakel Jul 22, 2026
09d0b9b
fix(profiles): resolve transcripts from current workspace
senamakel Jul 22, 2026
1085c6b
fix(profiles): persist default isolation settings
senamakel Jul 22, 2026
f326047
fix(profiles): preserve manually edited soul files
senamakel Jul 22, 2026
1907099
fix(profiles): apply default soul to live prompts
senamakel Jul 22, 2026
42c67f7
fix(security): protect scoped profile state
senamakel Jul 22, 2026
6982909
fix(threads): read profile-scoped transcripts
senamakel Jul 22, 2026
0d41978
fix(security): satisfy strict path lint
senamakel Jul 22, 2026
f899fad
Merge remote-tracking branch 'upstream/main' into pr/5118
senamakel Jul 22, 2026
6e37e63
fix(tools): guard node processes across profiles
senamakel Jul 22, 2026
be5c4e2
test(composio): isolate contract cache coverage
senamakel Jul 22, 2026
91f3b9a
fix(security): guard every active profile
senamakel Jul 22, 2026
e0c1a56
fix(threads): include scoped usage transcripts
senamakel Jul 22, 2026
05e9f8f
fix(experience): scope generated ids by profile
senamakel Jul 22, 2026
f6f1790
fix(skills): allow private workflow names and slugs
senamakel Jul 22, 2026
871ced5
fix(cron): fail closed for attributed profile builds
senamakel Jul 22, 2026
1aecb08
fix(agent): load profile memory in live sessions
senamakel Jul 22, 2026
d49a66c
fix(profiles): reject invalid normalized ids
senamakel Jul 22, 2026
90211ea
fix(skills): resolve workflow display names
senamakel Jul 22, 2026
5744912
fix(experience): route RPCs to profile memory
senamakel Jul 22, 2026
9e3548a
fix(agent): make profile soul override root identity
senamakel Jul 22, 2026
2d8eeba
fix(skills): prefer private workflows over builtins
senamakel Jul 22, 2026
ef0ddc2
fix(experience): merge shared and profile stores
senamakel Jul 22, 2026
686202b
fix(agent): resume from profile transcript scope
senamakel Jul 22, 2026
9a3cf77
fix(profiles): clear synced soul files
senamakel Jul 22, 2026
c34d5e7
fix(profiles): align file write approval workspace
senamakel Jul 22, 2026
25636b3
fix(profiles): invalidate sessions on home edits
senamakel Jul 22, 2026
e3dc87c
fix(profiles): scope workflow run logs
senamakel Jul 22, 2026
118a9d8
fix(profiles): persist cleared soul tombstone
senamakel Jul 22, 2026
7471245
fix(profiles): enforce gates for cron runs
senamakel Jul 22, 2026
6b2cb27
fix(profiles): scope detached workflow awaits
senamakel Jul 23, 2026
810d5ac
fix(profiles): preserve default root soul
senamakel Jul 23, 2026
0d7fc33
fix(profiles): replace soul files on windows
senamakel Jul 23, 2026
6287656
fix(profiles): track shell cwd in write guard
senamakel Jul 23, 2026
c985535
fix(skills): limit display aliases to profiles
senamakel Jul 23, 2026
502a7ef
fix(profiles): guard bare sibling operands
senamakel Jul 23, 2026
f86a8f3
fix(cron): apply profile runtime defaults
senamakel Jul 23, 2026
7d5ec42
fix(security): resolve relative policy paths from action root
senamakel Jul 23, 2026
995f09a
fix(profiles): block symlinked process cwd
senamakel Jul 23, 2026
59ccc8c
fix(profiles): protect shared profiles root
senamakel Jul 23, 2026
c4ba7c5
fix(profiles): preserve tool limits in subagents
senamakel Jul 23, 2026
155ad52
test(profiles): seed non-default built-in soul
senamakel Jul 23, 2026
4054b94
fix(security): retain sensitive filename denies
senamakel Jul 23, 2026
6280851
Merge remote-tracking branch 'upstream/main' into pr/5118
senamakel Jul 23, 2026
ad4df38
test(composio): preserve upstream contract case
senamakel Jul 23, 2026
7cc90a4
fix(profiles): merge shared experience recall
senamakel Jul 23, 2026
29b5b46
fix(profiles): enforce workflow memory scope
senamakel Jul 23, 2026
7b25d03
fix(profiles): guard bare profiles root
senamakel Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/src/components/settings/panels/CronJobsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ vi.mock('../hooks/useSettingsNavigation', () => ({
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
}));

// ── Mock Redux store hooks ──────────────────────────────────────────────
// The panel dispatches loadAgentProfiles + reads the agentProfile slice to
// feed the attribution picker / job-list labels. These tests render the panel
// without a Provider, so stub the hooks (profile UI is covered by the
// CronJobFormModal / CoreJobList component tests).
const noopDispatch = vi.fn();
vi.mock('../../../store/hooks', () => ({
useAppDispatch: () => noopDispatch,
useAppSelector: () => [],
}));

// ── Mock SettingsHeader ─────────────────────────────────────────────────
vi.mock('../components/SettingsHeader', () => ({
default: ({ title }: { title: string }) => <div data-testid="settings-header">{title}</div>,
Expand Down
12 changes: 12 additions & 0 deletions app/src/components/settings/panels/CronJobsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import createDebug from 'debug';
import { useCallback, useEffect, useState } from 'react';

import { useT } from '../../../lib/i18n/I18nContext';
import { loadAgentProfiles, selectAgentProfiles } from '../../../store/agentProfileSlice';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import {
type CoreCronJob,
type CoreCronRun,
Expand All @@ -23,6 +25,8 @@ const loadCronJobsLog = createDebug('app:settings:CronJobsPanel:loadCronSkills')

const CronJobsPanel = () => {
const { t } = useT();
const dispatch = useAppDispatch();
const profiles = useAppSelector(selectAgentProfiles);
const formatCronError = useCallback(
(key: string, message: string) => t(key).replace('{message}', message),
[t]
Expand Down Expand Up @@ -70,6 +74,11 @@ const CronJobsPanel = () => {
void loadCoreCronJobsOnly();
}, [loadCoreCronJobsOnly]);

// Populate the agent-profile attribution picker + job-list labels.
useEffect(() => {
void dispatch(loadAgentProfiles());
}, [dispatch]);

const toggleCoreJob = async (job: CoreCronJob) => {
const key = `core-toggle:${job.id}`;
setCoreBusyKey(key);
Expand Down Expand Up @@ -207,6 +216,7 @@ const CronJobsPanel = () => {
<CoreJobList
loading={loading}
coreJobs={coreJobs}
profiles={profiles}
coreRunsByJob={coreRunsByJob}
coreBusyKey={coreBusyKey}
onToggleCoreJob={job => void toggleCoreJob(job)}
Expand Down Expand Up @@ -235,6 +245,7 @@ const CronJobsPanel = () => {
key="cron-form-create"
mode="create"
open={true}
profiles={profiles}
onClose={() => setFormOpen(false)}
onCreate={params => handleCreate(params)}
onUpdate={handleUpdate}
Expand All @@ -248,6 +259,7 @@ const CronJobsPanel = () => {
mode="edit"
job={editingJob}
open={true}
profiles={profiles}
onClose={() => setEditingJob(null)}
onCreate={handleCreate}
onUpdate={handleUpdate}
Expand Down
58 changes: 58 additions & 0 deletions app/src/components/settings/panels/ProfileEditorPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,62 @@ describe('ProfileEditorPage', () => {
await waitFor(() => expect(mockUpsert).toHaveBeenCalled());
expect(mockUpsert.mock.calls[0][0].includeAgentConversations).toBe(false);
});

it('defaults the dedicated memory/workspace toggles to off and dispatches them true when flipped', async () => {
renderAt('/settings/profiles/new');
const dedicatedMemory = screen.getByLabelText('Dedicated memory');
const dedicatedWorkspace = screen.getByLabelText('Dedicated workspace');
expect(dedicatedMemory).toHaveAttribute('aria-checked', 'false');
expect(dedicatedWorkspace).toHaveAttribute('aria-checked', 'false');

fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Isolated' } });
fireEvent.click(dedicatedMemory);
fireEvent.click(dedicatedWorkspace);
expect(screen.getByLabelText('Dedicated memory')).toHaveAttribute('aria-checked', 'true');
expect(screen.getByLabelText('Dedicated workspace')).toHaveAttribute('aria-checked', 'true');
fireEvent.click(screen.getByText('Create'));

await waitFor(() => expect(mockUpsert).toHaveBeenCalled());
expect(mockUpsert.mock.calls[0][0].dedicatedMemory).toBe(true);
expect(mockUpsert.mock.calls[0][0].dedicatedWorkspace).toBe(true);
});

it('edit mode hydrates the dedicated toggles and shows the resolved read-only paths', () => {
renderAt('/settings/profiles/edit/writer', [
profile({
id: 'writer',
name: 'Writer',
dedicatedMemory: true,
dedicatedWorkspace: true,
soulMdFile: '/workspace/personalities/writer/SOUL.md',
workspaceDir: '/action/profiles/writer',
}),
]);
expect(screen.getByLabelText('Dedicated memory')).toHaveAttribute('aria-checked', 'true');
expect(screen.getByLabelText('Dedicated workspace')).toHaveAttribute('aria-checked', 'true');
expect(screen.getByText('/workspace/personalities/writer/SOUL.md')).toBeInTheDocument();
expect(screen.getByText('/action/profiles/writer')).toBeInTheDocument();
});

it('hides the resolved read-only path rows when the profile has none', () => {
renderAt('/settings/profiles/edit/writer', [profile({ id: 'writer' })]);
expect(screen.queryByText('SOUL.md file')).not.toBeInTheDocument();
expect(screen.queryByText('Workspace directory')).not.toBeInTheDocument();
expect(screen.queryByText('Skills directory')).not.toBeInTheDocument();
});

it('shows the resolved skills directory path and hint when present', () => {
renderAt('/settings/profiles/edit/writer', [
profile({
id: 'writer',
name: 'Writer',
skillsDir: '/workspace/personalities/writer/skills',
}),
]);
expect(screen.getByText('Skills directory')).toBeInTheDocument();
expect(screen.getByText('/workspace/personalities/writer/skills')).toBeInTheDocument();
expect(
screen.getByText('SKILL.md files placed here are private to this profile.')
).toBeInTheDocument();
});
});
66 changes: 66 additions & 0 deletions app/src/components/settings/panels/ProfileEditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const ProfileEditorPage = () => {
const [composioIntegrations, setComposioIntegrations] = useState<Allowlist>(null);
const [allowedSkills, setAllowedSkills] = useState<Allowlist>(null);
const [allowedMcpServers, setAllowedMcpServers] = useState<Allowlist>(null);
const [dedicatedMemory, setDedicatedMemory] = useState(false);
const [dedicatedWorkspace, setDedicatedWorkspace] = useState(false);

const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -120,6 +122,8 @@ const ProfileEditorPage = () => {
setComposioIntegrations(existing.composioIntegrations ?? null);
setAllowedSkills(existing.allowedSkills ?? null);
setAllowedMcpServers(existing.allowedMcpServers ?? null);
setDedicatedMemory(existing.dedicatedMemory ?? false);
setDedicatedWorkspace(existing.dedicatedWorkspace ?? false);
}, [existing, isCreate, profiles.length]);

const handleName = (value: string) => {
Expand Down Expand Up @@ -159,6 +163,8 @@ const ProfileEditorPage = () => {
composioIntegrations,
allowedSkills,
allowedMcpServers,
dedicatedMemory,
dedicatedWorkspace,
builtIn: existing?.builtIn ?? false,
};
try {
Expand Down Expand Up @@ -348,6 +354,66 @@ const ProfileEditorPage = () => {
/>
</SettingsSection>

{/* Isolation */}
<SettingsSection>
<SettingsRow
label={t('settings.profiles.editor.dedicatedMemory')}
description={t('settings.profiles.editor.dedicatedMemoryHint')}
control={
<SettingsSwitch
id="profile-dedicated-memory"
checked={dedicatedMemory}
onCheckedChange={setDedicatedMemory}
aria-label={t('settings.profiles.editor.dedicatedMemory')}
/>
}
/>
<SettingsRow
label={t('settings.profiles.editor.dedicatedWorkspace')}
description={t('settings.profiles.editor.dedicatedWorkspaceHint')}
control={
<SettingsSwitch
id="profile-dedicated-workspace"
checked={dedicatedWorkspace}
onCheckedChange={setDedicatedWorkspace}
aria-label={t('settings.profiles.editor.dedicatedWorkspace')}
/>
}
/>
{existing?.soulMdFile && (
<SettingsRow
label={t('settings.profiles.editor.soulMdFile')}
control={
<code className="font-mono text-xs text-content-muted break-all">
{existing.soulMdFile}
</code>
}
/>
)}
{existing?.workspaceDir && (
<SettingsRow
label={t('settings.profiles.editor.workspaceDir')}
control={
<code className="font-mono text-xs text-content-muted break-all">
{existing.workspaceDir}
</code>
}
/>
)}
{existing?.skillsDir && (
<SettingsRow
label={t('settings.profiles.editor.skillsDir')}
description={t('settings.profiles.editor.skillsDirHint')}
stacked
control={
<code className="font-mono text-xs text-content-muted break-all">
{existing.skillsDir}
</code>
}
/>
)}
</SettingsSection>

{/* Capabilities */}
<SettingsSection>
<AllowlistField
Expand Down
45 changes: 45 additions & 0 deletions app/src/components/settings/panels/cron/CoreJobList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';

import type { AgentProfile } from '../../../../types/agentProfile';
import type { CoreCronJob, CoreCronRun } from '../../../../utils/tauriCommands';
import CoreJobList from './CoreJobList';

const profiles: AgentProfile[] = [
{ id: 'writer', name: 'Writer', description: '', agentId: 'orchestrator', builtIn: false },
];

vi.mock('../../../../lib/i18n/I18nContext', () => ({
useT: () => ({
t: (key: string) =>
Expand All @@ -15,6 +20,7 @@ vi.mock('../../../../lib/i18n/I18nContext', () => ({
'settings.cron.jobs.lastStatus': 'Last status',
'settings.cron.jobs.nextRun': 'Next run',
'settings.cron.jobs.pause': 'Pause',
'settings.cron.jobs.profile': 'Profile',
'settings.cron.jobs.recentRuns': 'Recent runs',
'settings.cron.jobs.saving': 'Saving…',
'settings.cron.jobs.schedule': 'Schedule',
Expand Down Expand Up @@ -108,6 +114,45 @@ describe('CoreJobList stable test hooks', () => {
expect(onEditCoreJob).toHaveBeenCalledWith(job);
});

test('shows the attributed profile name when it resolves', () => {
render(
<CoreJobList
loading={false}
coreJobs={[{ ...job, profile_id: 'writer' }]}
profiles={profiles}
coreRunsByJob={{}}
coreBusyKey={null}
onToggleCoreJob={vi.fn()}
onRunCoreJob={vi.fn()}
onLoadCoreRuns={vi.fn()}
onRemoveCoreJob={vi.fn()}
/>
);
expect(screen.getByTestId(`cron-job-profile-${job.id}`)).toHaveTextContent('Writer');
});

test('falls back to the raw profile id when the profile was deleted', () => {
render(
<CoreJobList
loading={false}
coreJobs={[{ ...job, profile_id: 'ghost' }]}
profiles={profiles}
coreRunsByJob={{}}
coreBusyKey={null}
onToggleCoreJob={vi.fn()}
onRunCoreJob={vi.fn()}
onLoadCoreRuns={vi.fn()}
onRemoveCoreJob={vi.fn()}
/>
);
expect(screen.getByTestId(`cron-job-profile-${job.id}`)).toHaveTextContent('ghost');
});

test('omits the profile row when the job has no attribution', () => {
renderList();
expect(screen.queryByTestId(`cron-job-profile-${job.id}`)).not.toBeInTheDocument();
});

test('toggle button shows saving label when coreBusyKey targets the toggle', () => {
render(
<CoreJobList
Expand Down
17 changes: 17 additions & 0 deletions app/src/components/settings/panels/cron/CoreJobList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useT } from '../../../../lib/i18n/I18nContext';
import type { AgentProfile } from '../../../../types/agentProfile';
import type { CoreCronJob, CoreCronRun } from '../../../../utils/tauriCommands';
import Button from '../../../ui/Button';

Expand All @@ -13,6 +14,8 @@ interface CoreJobListProps {
onRemoveCoreJob: (jobId: string) => void;
/** Optional: when provided, an Edit button is rendered per row. */
onEditCoreJob?: (job: CoreCronJob) => void;
/** Agent profiles, used to resolve a job's attributed profile name. */
profiles?: AgentProfile[];
}

const CoreJobList = ({
Expand All @@ -25,9 +28,15 @@ const CoreJobList = ({
onLoadCoreRuns,
onRemoveCoreJob,
onEditCoreJob,
profiles = [],
}: CoreJobListProps) => {
const { t } = useT();

// Resolve a job's attributed profile to a display name, falling back to the
// raw id when the profile has since been deleted.
const profileLabel = (profileId: string): string =>
profiles.find(p => p.id === profileId)?.name || profileId;

const toggleButtonLabel = (job: CoreCronJob) => {
if (coreBusyKey === `core-toggle:${job.id}`) {
return t('settings.cron.jobs.saving');
Expand Down Expand Up @@ -103,6 +112,14 @@ const CoreJobList = ({
{new Date(job.next_run).toLocaleString()}
</span>
</div>
{job.profile_id && (
<div data-testid={`cron-job-profile-${job.id}`}>
{t('settings.cron.jobs.profile')}{' '}
<span className="font-medium text-content-secondary">
{profileLabel(job.profile_id)}
</span>
</div>
)}
{job.last_status && (
<div>
{t('settings.cron.jobs.lastStatus')}{' '}
Expand Down
Loading
Loading