Compare commits
1 Commits
feat/ms22-
...
chore/ms21
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a742aa634 |
@@ -1,19 +1,17 @@
|
|||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
import { WorkspaceMemberRole } from "@mosaic/shared";
|
import { WorkspaceMemberRole } from "@mosaic/shared";
|
||||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type AdminUser,
|
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
inviteUser,
|
inviteUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
type AdminUsersResponse,
|
type AdminUsersResponse,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
import { useAuth } from "@/lib/auth/auth-context";
|
|
||||||
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
||||||
import UsersSettingsPage from "./page";
|
import UsersSettingsPage from "./page";
|
||||||
|
|
||||||
@@ -41,80 +39,48 @@ vi.mock("@/lib/api/workspaces", () => ({
|
|||||||
updateWorkspaceMemberRole: vi.fn(),
|
updateWorkspaceMemberRole: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/auth/auth-context", () => ({
|
|
||||||
useAuth: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const fetchAdminUsersMock = vi.mocked(fetchAdminUsers);
|
const fetchAdminUsersMock = vi.mocked(fetchAdminUsers);
|
||||||
const inviteUserMock = vi.mocked(inviteUser);
|
const inviteUserMock = vi.mocked(inviteUser);
|
||||||
const updateUserMock = vi.mocked(updateUser);
|
const updateUserMock = vi.mocked(updateUser);
|
||||||
const deactivateUserMock = vi.mocked(deactivateUser);
|
const deactivateUserMock = vi.mocked(deactivateUser);
|
||||||
const fetchUserWorkspacesMock = vi.mocked(fetchUserWorkspaces);
|
const fetchUserWorkspacesMock = vi.mocked(fetchUserWorkspaces);
|
||||||
const updateWorkspaceMemberRoleMock = vi.mocked(updateWorkspaceMemberRole);
|
const updateWorkspaceMemberRoleMock = vi.mocked(updateWorkspaceMemberRole);
|
||||||
const useAuthMock = vi.mocked(useAuth);
|
|
||||||
|
|
||||||
function makeAdminUser(overrides?: Partial<AdminUser>): AdminUser {
|
const adminUsersResponse: AdminUsersResponse = {
|
||||||
return {
|
data: [
|
||||||
id: "user-1",
|
{
|
||||||
name: "Alice",
|
id: "user-1",
|
||||||
email: "alice@example.com",
|
name: "Alice",
|
||||||
emailVerified: true,
|
email: "alice@example.com",
|
||||||
image: null,
|
emailVerified: true,
|
||||||
createdAt: "2026-01-01T00:00:00.000Z",
|
image: null,
|
||||||
deactivatedAt: null,
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
isLocalAuth: false,
|
deactivatedAt: null,
|
||||||
invitedAt: null,
|
isLocalAuth: false,
|
||||||
invitedBy: null,
|
invitedAt: null,
|
||||||
workspaceMemberships: [
|
invitedBy: null,
|
||||||
{
|
workspaceMemberships: [
|
||||||
workspaceId: "workspace-1",
|
{
|
||||||
workspaceName: "Personal Workspace",
|
workspaceId: "workspace-1",
|
||||||
role: WorkspaceMemberRole.ADMIN,
|
workspaceName: "Personal Workspace",
|
||||||
joinedAt: "2026-01-01T00:00:00.000Z",
|
role: WorkspaceMemberRole.ADMIN,
|
||||||
},
|
joinedAt: "2026-01-01T00:00:00.000Z",
|
||||||
],
|
},
|
||||||
...overrides,
|
],
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeAdminUsersResponse(options?: {
|
|
||||||
data?: AdminUser[];
|
|
||||||
page?: number;
|
|
||||||
totalPages?: number;
|
|
||||||
total?: number;
|
|
||||||
limit?: number;
|
|
||||||
}): AdminUsersResponse {
|
|
||||||
const data = options?.data ?? [makeAdminUser()];
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total: options?.total ?? data.length,
|
|
||||||
page: options?.page ?? 1,
|
|
||||||
limit: options?.limit ?? 50,
|
|
||||||
totalPages: options?.totalPages ?? 1,
|
|
||||||
},
|
},
|
||||||
};
|
],
|
||||||
}
|
meta: {
|
||||||
|
total: 1,
|
||||||
function makeAuthState(userId: string): ReturnType<typeof useAuth> {
|
page: 1,
|
||||||
return {
|
limit: 50,
|
||||||
user: { id: userId, email: `${userId}@example.com`, name: "Current User" },
|
totalPages: 1,
|
||||||
isLoading: false,
|
},
|
||||||
isAuthenticated: true,
|
};
|
||||||
authError: null,
|
|
||||||
sessionExpiring: false,
|
|
||||||
sessionMinutesRemaining: 0,
|
|
||||||
signOut: vi.fn(() => Promise.resolve()),
|
|
||||||
refreshSession: vi.fn(() => Promise.resolve()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("UsersSettingsPage", () => {
|
describe("UsersSettingsPage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
const adminUsersResponse = makeAdminUsersResponse();
|
|
||||||
|
|
||||||
fetchAdminUsersMock.mockResolvedValue(adminUsersResponse);
|
fetchAdminUsersMock.mockResolvedValue(adminUsersResponse);
|
||||||
fetchUserWorkspacesMock.mockResolvedValue([
|
fetchUserWorkspacesMock.mockResolvedValue([
|
||||||
{
|
{
|
||||||
@@ -131,7 +97,10 @@ describe("UsersSettingsPage", () => {
|
|||||||
email: "new@example.com",
|
email: "new@example.com",
|
||||||
invitedAt: "2026-01-02T00:00:00.000Z",
|
invitedAt: "2026-01-02T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
const firstUser = adminUsersResponse.data[0] ?? makeAdminUser();
|
const firstUser = adminUsersResponse.data[0];
|
||||||
|
if (!firstUser) {
|
||||||
|
throw new Error("Expected at least one admin user in test fixtures");
|
||||||
|
}
|
||||||
|
|
||||||
updateUserMock.mockResolvedValue(firstUser);
|
updateUserMock.mockResolvedValue(firstUser);
|
||||||
deactivateUserMock.mockResolvedValue(firstUser);
|
deactivateUserMock.mockResolvedValue(firstUser);
|
||||||
@@ -147,8 +116,6 @@ describe("UsersSettingsPage", () => {
|
|||||||
image: null,
|
image: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useAuthMock.mockReturnValue(makeAuthState("user-current"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows access denied to non-admin users", async () => {
|
it("shows access denied to non-admin users", async () => {
|
||||||
@@ -207,146 +174,4 @@ describe("UsersSettingsPage", () => {
|
|||||||
|
|
||||||
expect(updateWorkspaceMemberRoleMock).not.toHaveBeenCalled();
|
expect(updateWorkspaceMemberRoleMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("caps pagination to the last valid page after deactivation shrinks the dataset", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const pageOneUser = makeAdminUser({
|
|
||||||
id: "user-1",
|
|
||||||
name: "Alice",
|
|
||||||
email: "alice@example.com",
|
|
||||||
});
|
|
||||||
const pageTwoUser = makeAdminUser({
|
|
||||||
id: "user-2",
|
|
||||||
name: "Bob",
|
|
||||||
email: "bob@example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchAdminUsersMock.mockReset();
|
|
||||||
const responses = [
|
|
||||||
{
|
|
||||||
expectedPage: 1,
|
|
||||||
response: makeAdminUsersResponse({
|
|
||||||
data: [pageOneUser],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 2,
|
|
||||||
total: 2,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectedPage: 2,
|
|
||||||
response: makeAdminUsersResponse({
|
|
||||||
data: [pageTwoUser],
|
|
||||||
page: 2,
|
|
||||||
totalPages: 2,
|
|
||||||
total: 2,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectedPage: 2,
|
|
||||||
response: makeAdminUsersResponse({
|
|
||||||
data: [],
|
|
||||||
page: 2,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectedPage: 1,
|
|
||||||
response: makeAdminUsersResponse({
|
|
||||||
data: [pageOneUser],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 1,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
fetchAdminUsersMock.mockImplementation((page = 1) => {
|
|
||||||
const next = responses.shift();
|
|
||||||
if (!next) {
|
|
||||||
throw new Error("Unexpected fetchAdminUsers call in pagination-cap test");
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(page).toBe(next.expectedPage);
|
|
||||||
return Promise.resolve(next.response);
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<UsersSettingsPage />);
|
|
||||||
|
|
||||||
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
||||||
expect(await screen.findByText("bob@example.com")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const pageTwoRow = screen.getByText("bob@example.com").closest('[role="button"]');
|
|
||||||
if (!(pageTwoRow instanceof HTMLElement)) {
|
|
||||||
throw new Error("Expected Bob's row to exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.click(within(pageTwoRow).getByRole("button", { name: "Deactivate" }));
|
|
||||||
const deactivateButtons = await screen.findAllByRole("button", { name: "Deactivate" });
|
|
||||||
const confirmDeactivateButton = deactivateButtons[deactivateButtons.length - 1];
|
|
||||||
if (!confirmDeactivateButton) {
|
|
||||||
throw new Error("Expected confirmation deactivate button to be rendered");
|
|
||||||
}
|
|
||||||
await user.click(confirmDeactivateButton);
|
|
||||||
|
|
||||||
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("No Users Yet")).not.toBeInTheDocument();
|
|
||||||
expect(deactivateUserMock).toHaveBeenCalledWith("user-2");
|
|
||||||
const requestedPages = fetchAdminUsersMock.mock.calls.map(([requestedPage]) => requestedPage);
|
|
||||||
expect(requestedPages.slice(-2)).toEqual([2, 1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the API error state without rendering the empty-state message", async () => {
|
|
||||||
fetchAdminUsersMock.mockRejectedValueOnce(new Error("Unable to load users"));
|
|
||||||
|
|
||||||
render(<UsersSettingsPage />);
|
|
||||||
|
|
||||||
expect(await screen.findByText("Unable to load users")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("No Users Yet")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("Invite the first user to get started.")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prevents the current user from deactivating their own account", async () => {
|
|
||||||
useAuthMock.mockReturnValue(makeAuthState("user-1"));
|
|
||||||
|
|
||||||
const selfUser = makeAdminUser({
|
|
||||||
id: "user-1",
|
|
||||||
name: "Alice",
|
|
||||||
email: "alice@example.com",
|
|
||||||
});
|
|
||||||
const otherUser = makeAdminUser({
|
|
||||||
id: "user-2",
|
|
||||||
name: "Bob",
|
|
||||||
email: "bob@example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchAdminUsersMock.mockResolvedValueOnce(
|
|
||||||
makeAdminUsersResponse({
|
|
||||||
data: [selfUser, otherUser],
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
total: 2,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<UsersSettingsPage />);
|
|
||||||
|
|
||||||
expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("bob@example.com")).toBeInTheDocument();
|
|
||||||
|
|
||||||
const selfRow = screen.getByText("alice@example.com").closest('[role="button"]');
|
|
||||||
if (!(selfRow instanceof HTMLElement)) {
|
|
||||||
throw new Error("Expected current-user row to exist");
|
|
||||||
}
|
|
||||||
expect(within(selfRow).queryByRole("button", { name: "Deactivate" })).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const otherRow = screen.getByText("bob@example.com").closest('[role="button"]');
|
|
||||||
if (!(otherRow instanceof HTMLElement)) {
|
|
||||||
throw new Error("Expected other-user row to exist");
|
|
||||||
}
|
|
||||||
expect(within(otherRow).getByRole("button", { name: "Deactivate" })).toBeInTheDocument();
|
|
||||||
expect(deactivateUserMock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
type InviteUserDto,
|
type InviteUserDto,
|
||||||
type UpdateUserDto,
|
type UpdateUserDto,
|
||||||
} from "@/lib/api/admin";
|
} from "@/lib/api/admin";
|
||||||
import { useAuth } from "@/lib/auth/auth-context";
|
|
||||||
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
import { fetchUserWorkspaces, updateWorkspaceMemberRole } from "@/lib/api/workspaces";
|
||||||
import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied";
|
import { SettingsAccessDenied } from "@/components/settings/SettingsAccessDenied";
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ const INITIAL_DETAIL_FORM = {
|
|||||||
workspaceId: null as string | null,
|
workspaceId: null as string | null,
|
||||||
workspaceName: null as string | null,
|
workspaceName: null as string | null,
|
||||||
};
|
};
|
||||||
const USERS_PAGE_SIZE = 50;
|
|
||||||
|
|
||||||
interface DetailInitialState {
|
interface DetailInitialState {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -106,11 +104,8 @@ function getPrimaryMembership(user: AdminUser): AdminWorkspaceMembership | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersSettingsPage(): ReactElement {
|
export default function UsersSettingsPage(): ReactElement {
|
||||||
const { user: authUser } = useAuth();
|
|
||||||
|
|
||||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||||
const [meta, setMeta] = useState<AdminUsersResponse["meta"] | null>(null);
|
const [meta, setMeta] = useState<AdminUsersResponse["meta"] | null>(null);
|
||||||
const [page, setPage] = useState<number>(1);
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -132,35 +127,25 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
const [deactivateTarget, setDeactivateTarget] = useState<AdminUser | null>(null);
|
const [deactivateTarget, setDeactivateTarget] = useState<AdminUser | null>(null);
|
||||||
const [isDeactivating, setIsDeactivating] = useState<boolean>(false);
|
const [isDeactivating, setIsDeactivating] = useState<boolean>(false);
|
||||||
|
|
||||||
const loadUsers = useCallback(
|
const loadUsers = useCallback(async (showLoadingState: boolean): Promise<void> => {
|
||||||
async (showLoadingState: boolean): Promise<void> => {
|
try {
|
||||||
try {
|
if (showLoadingState) {
|
||||||
if (showLoadingState) {
|
setIsLoading(true);
|
||||||
setIsLoading(true);
|
} else {
|
||||||
} else {
|
setIsRefreshing(true);
|
||||||
setIsRefreshing(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchAdminUsers(page, USERS_PAGE_SIZE);
|
|
||||||
const lastValidPage = Math.max(1, response.meta.totalPages);
|
|
||||||
|
|
||||||
if (page > lastValidPage) {
|
|
||||||
setPage(lastValidPage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUsers(response.data);
|
|
||||||
setMeta(response.meta);
|
|
||||||
setError(null);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
setError(err instanceof Error ? err.message : "Failed to load admin users");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[page]
|
const response = await fetchAdminUsers(1, 50);
|
||||||
);
|
setUsers(response.data);
|
||||||
|
setMeta(response.meta);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to load admin users");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserWorkspaces()
|
fetchUserWorkspaces()
|
||||||
@@ -185,7 +170,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loadUsers(true);
|
void loadUsers(true);
|
||||||
}, [isAdmin, loadUsers, page]);
|
}, [isAdmin, loadUsers]);
|
||||||
|
|
||||||
function resetInviteForm(): void {
|
function resetInviteForm(): void {
|
||||||
setInviteForm(INITIAL_INVITE_FORM);
|
setInviteForm(INITIAL_INVITE_FORM);
|
||||||
@@ -339,12 +324,6 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authUser?.id === deactivateTarget.id) {
|
|
||||||
setDeactivateTarget(null);
|
|
||||||
setError("You cannot deactivate your own account.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeactivating(true);
|
setIsDeactivating(true);
|
||||||
await deactivateUser(deactivateTarget.id);
|
await deactivateUser(deactivateTarget.id);
|
||||||
@@ -502,13 +481,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{error ? (
|
||||||
<Card>
|
|
||||||
<CardContent className="py-12 text-center text-muted-foreground">
|
|
||||||
Loading users...
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : error ? (
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-4">
|
<CardContent className="py-4">
|
||||||
<p className="text-sm text-destructive" role="alert">
|
<p className="text-sm text-destructive" role="alert">
|
||||||
@@ -516,6 +489,14 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
|
Loading users...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
) : users.length === 0 ? (
|
) : users.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -533,7 +514,6 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
{users.map((user) => {
|
{users.map((user) => {
|
||||||
const primaryMembership = getPrimaryMembership(user);
|
const primaryMembership = getPrimaryMembership(user);
|
||||||
const isActive = user.deactivatedAt === null;
|
const isActive = user.deactivatedAt === null;
|
||||||
const isCurrentUser = authUser?.id === user.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -549,14 +529,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 min-w-0">
|
<div className="space-y-1 min-w-0">
|
||||||
<p className="font-semibold truncate">
|
<p className="font-semibold truncate">{user.name || "Unnamed User"}</p>
|
||||||
{user.name || "Unnamed User"}
|
|
||||||
{isCurrentUser ? (
|
|
||||||
<span className="ml-2 text-xs font-normal text-muted-foreground">
|
|
||||||
(You)
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
|
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -567,7 +540,7 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
<Badge variant={isActive ? "secondary" : "destructive"}>
|
<Badge variant={isActive ? "secondary" : "destructive"}>
|
||||||
{isActive ? "Active" : "Inactive"}
|
{isActive ? "Active" : "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isActive && !isCurrentUser ? (
|
{isActive ? (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -584,36 +557,6 @@ export default function UsersSettingsPage(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{meta && meta.totalPages > 1 ? (
|
|
||||||
<div className="flex items-center justify-between pt-3 mt-1 border-t">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Page {page} of {meta.totalPages}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={page === 1}
|
|
||||||
onClick={() => {
|
|
||||||
setPage((previousPage) => Math.max(1, previousPage - 1));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={page >= meta.totalPages}
|
|
||||||
onClick={() => {
|
|
||||||
setPage((previousPage) => Math.min(meta.totalPages, previousPage + 1));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,49 +1,90 @@
|
|||||||
# Mosaic Agent Fleet
|
# OpenClaw Agent Fleet
|
||||||
|
|
||||||
Multi-agent deployment for Mosaic Stack using OpenClaw containers on Docker Swarm.
|
OpenClaw multi-agent deployment for Mosaic Stack using Docker Swarm and Portainer.
|
||||||
|
|
||||||
## Architecture
|
## Agent Roster
|
||||||
|
|
||||||
Each agent runs as an isolated OpenClaw Gateway instance with its own:
|
| Agent | Service | Primary Model | Role |
|
||||||
|
| ----------------- | ------------------- | --------------- | ---------------------------------- |
|
||||||
- **Workspace** — persistent volume for agent files and memory
|
| jarvis-main | `jarvis-main` | `zai/glm-5` | Orchestrator / user-facing gateway |
|
||||||
- **State** — persistent volume for auth tokens and sessions
|
| jarvis-projects | `jarvis-projects` | `zai/glm-5` | Development and coding tasks |
|
||||||
- **Config** — template rendered at startup from environment variables
|
| jarvis-research | `jarvis-research` | `zai/glm-5` | Research and web search |
|
||||||
|
| jarvis-operations | `jarvis-operations` | `ollama/cogito` | Monitoring, health checks, alerts |
|
||||||
Agents communicate with the Mosaic API via the OpenAI-compatible
|
|
||||||
`/v1/chat/completions` endpoint. The Mosaic WebUI routes chat requests
|
|
||||||
to agents through the `OpenClawGatewayModule`.
|
|
||||||
|
|
||||||
## Default Agent Roles
|
|
||||||
|
|
||||||
| Agent | Role | Description |
|
|
||||||
| ----------------- | ------------ | ------------------------------------------- |
|
|
||||||
| mosaic-main | Orchestrator | User-facing gateway, routes to other agents |
|
|
||||||
| mosaic-projects | Developer | Implementation, coding, PRs |
|
|
||||||
| mosaic-research | Research | Web search, analysis, discovery |
|
|
||||||
| mosaic-operations | Operations | Monitoring, health checks, alerts |
|
|
||||||
|
|
||||||
> **Models and providers are configured per-deployment** via environment
|
|
||||||
> variables and the Mosaic Settings UI — not hardcoded in these files.
|
|
||||||
> See the [Setup Guide](openclaw-instances/README.md) for env var reference.
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Docker Swarm initialized on target host
|
1. Docker Swarm initialized on the target host
|
||||||
- Mosaic Stack running (`mosaic-stack_internal` network available)
|
2. Existing Docker network `mosaic-stack_internal` (external swarm network)
|
||||||
- At least one LLM provider API key (Z.ai, OpenAI, Anthropic, etc.)
|
3. Z.ai API access key (`ZAI_API_KEY`)
|
||||||
|
4. Ollama reachable at `10.1.1.42:11434` for the `cogito` model
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. **Configure** — Fill in `docker/openclaw-instances/*.env` files
|
### 1. Configure each agent env file
|
||||||
2. **Deploy** — `docker stack deploy -c docker/openclaw-compose.yml mosaic-agents`
|
|
||||||
3. **Auth** — If needed, run `openclaw auth` inside a container (or via Mosaic terminal)
|
|
||||||
4. **Verify** — `docker stack services mosaic-agents`
|
|
||||||
|
|
||||||
See [openclaw-instances/README.md](openclaw-instances/README.md) for detailed setup.
|
Set values in:
|
||||||
|
|
||||||
## Future: Onboarding Wizard
|
- `docker/openclaw-instances/jarvis-main.env`
|
||||||
|
- `docker/openclaw-instances/jarvis-projects.env`
|
||||||
|
- `docker/openclaw-instances/jarvis-research.env`
|
||||||
|
- `docker/openclaw-instances/jarvis-operations.env`
|
||||||
|
|
||||||
Model assignments, provider configuration, and agent customization will be
|
Required variables:
|
||||||
managed through the Mosaic WebUI onboarding wizard and Settings pages (MS22-P4).
|
|
||||||
Until then, use environment variables per the README.
|
- `OPENCLAW_CONFIG_PATH=/config/openclaw.json`
|
||||||
|
- `ZAI_API_KEY=<your-zai-api-key>`
|
||||||
|
- `OPENCLAW_GATEWAY_TOKEN=<unique-token-per-agent>`
|
||||||
|
|
||||||
|
### 2. Generate unique gateway tokens
|
||||||
|
|
||||||
|
Generate one token per service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy the fleet
|
||||||
|
|
||||||
|
From repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stack deploy -c docker/openclaw-compose.yml jarvis
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify service status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stack services jarvis
|
||||||
|
docker service logs jarvis-jarvis-main --tail 100
|
||||||
|
docker service logs jarvis-jarvis-projects --tail 100
|
||||||
|
docker service logs jarvis-jarvis-research --tail 100
|
||||||
|
docker service logs jarvis-jarvis-operations --tail 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. First-time auth (if required)
|
||||||
|
|
||||||
|
Exec into a container and run OpenClaw auth device flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it $(docker ps -q -f name=jarvis-jarvis-main) sh
|
||||||
|
openclaw auth
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also complete this in the Mosaic WebUI terminal (xterm.js).
|
||||||
|
|
||||||
|
## Management Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ----------------------------------------------------------- | ---------------------- |
|
||||||
|
| `docker stack deploy -c docker/openclaw-compose.yml jarvis` | Deploy/update fleet |
|
||||||
|
| `docker stack services jarvis` | List services in stack |
|
||||||
|
| `docker service logs jarvis-<service>` | View service logs |
|
||||||
|
| `docker service update --force jarvis-<service>` | Restart rolling update |
|
||||||
|
| `docker service scale jarvis-<service>=N` | Scale a service |
|
||||||
|
| `docker stack rm jarvis` | Remove fleet |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Each service stores persistent local OpenClaw state in `/home/node/.openclaw`.
|
||||||
|
- Each service mounts a read-only per-agent JSON config at `/config/openclaw.json`.
|
||||||
|
- `chatCompletions` endpoint is enabled in each agent config for Mosaic API usage.
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
# Mosaic Agent Fleet — OpenClaw Docker Swarm Stack
|
|
||||||
# Deploy: docker stack deploy -c docker/openclaw-compose.yml mosaic-agents
|
|
||||||
# All config via env vars — see openclaw-instances/*.env
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mosaic-main:
|
jarvis-main:
|
||||||
image: alpine/openclaw:latest
|
image: alpine/openclaw:latest
|
||||||
command: ["/config/entrypoint.sh"]
|
command: ["gateway", "run", "--bind", "lan", "--auth", "token"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./openclaw-instances/mosaic-main.env
|
- ./openclaw-instances/jarvis-main.env
|
||||||
environment:
|
environment:
|
||||||
OPENCLAW_CONFIG_PATH: /tmp/openclaw.json
|
OPENCLAW_CONFIG_PATH: /config/openclaw.json
|
||||||
volumes:
|
volumes:
|
||||||
- mosaic-main-config:/config:ro
|
- jarvis-main-config:/config/openclaw.json:ro
|
||||||
- mosaic-main-state:/home/node/.openclaw
|
- jarvis-main-state:/home/node/.openclaw
|
||||||
networks:
|
networks:
|
||||||
- mosaic-stack_internal
|
- mosaic-stack_internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -33,19 +29,19 @@ services:
|
|||||||
reservations:
|
reservations:
|
||||||
memory: 512M
|
memory: 512M
|
||||||
labels:
|
labels:
|
||||||
- com.mosaic.agent=mosaic-main
|
- com.mosaic.agent=jarvis-main
|
||||||
- com.mosaic.role=orchestrator
|
- com.mosaic.role=orchestrator
|
||||||
|
|
||||||
mosaic-projects:
|
jarvis-projects:
|
||||||
image: alpine/openclaw:latest
|
image: alpine/openclaw:latest
|
||||||
command: ["/config/entrypoint.sh"]
|
command: ["gateway", "run", "--bind", "lan", "--auth", "token"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./openclaw-instances/mosaic-projects.env
|
- ./openclaw-instances/jarvis-projects.env
|
||||||
environment:
|
environment:
|
||||||
OPENCLAW_CONFIG_PATH: /tmp/openclaw.json
|
OPENCLAW_CONFIG_PATH: /config/openclaw.json
|
||||||
volumes:
|
volumes:
|
||||||
- mosaic-projects-config:/config:ro
|
- jarvis-projects-config:/config/openclaw.json:ro
|
||||||
- mosaic-projects-state:/home/node/.openclaw
|
- jarvis-projects-state:/home/node/.openclaw
|
||||||
networks:
|
networks:
|
||||||
- mosaic-stack_internal
|
- mosaic-stack_internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -58,7 +54,7 @@ services:
|
|||||||
replicas: 1
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
delay: 10s
|
delay: 5s
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -66,19 +62,19 @@ services:
|
|||||||
reservations:
|
reservations:
|
||||||
memory: 1G
|
memory: 1G
|
||||||
labels:
|
labels:
|
||||||
- com.mosaic.agent=mosaic-projects
|
- com.mosaic.agent=jarvis-projects
|
||||||
- com.mosaic.role=developer
|
- com.mosaic.role=development
|
||||||
|
|
||||||
mosaic-research:
|
jarvis-research:
|
||||||
image: alpine/openclaw:latest
|
image: alpine/openclaw:latest
|
||||||
command: ["/config/entrypoint.sh"]
|
command: ["gateway", "run", "--bind", "lan", "--auth", "token"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./openclaw-instances/mosaic-research.env
|
- ./openclaw-instances/jarvis-research.env
|
||||||
environment:
|
environment:
|
||||||
OPENCLAW_CONFIG_PATH: /tmp/openclaw.json
|
OPENCLAW_CONFIG_PATH: /config/openclaw.json
|
||||||
volumes:
|
volumes:
|
||||||
- mosaic-research-config:/config:ro
|
- jarvis-research-config:/config/openclaw.json:ro
|
||||||
- mosaic-research-state:/home/node/.openclaw
|
- jarvis-research-state:/home/node/.openclaw
|
||||||
networks:
|
networks:
|
||||||
- mosaic-stack_internal
|
- mosaic-stack_internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -91,7 +87,7 @@ services:
|
|||||||
replicas: 1
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
delay: 10s
|
delay: 5s
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -99,19 +95,19 @@ services:
|
|||||||
reservations:
|
reservations:
|
||||||
memory: 256M
|
memory: 256M
|
||||||
labels:
|
labels:
|
||||||
- com.mosaic.agent=mosaic-research
|
- com.mosaic.agent=jarvis-research
|
||||||
- com.mosaic.role=research
|
- com.mosaic.role=research
|
||||||
|
|
||||||
mosaic-operations:
|
jarvis-operations:
|
||||||
image: alpine/openclaw:latest
|
image: alpine/openclaw:latest
|
||||||
command: ["/config/entrypoint.sh"]
|
command: ["gateway", "run", "--bind", "lan", "--auth", "token"]
|
||||||
env_file:
|
env_file:
|
||||||
- ./openclaw-instances/mosaic-operations.env
|
- ./openclaw-instances/jarvis-operations.env
|
||||||
environment:
|
environment:
|
||||||
OPENCLAW_CONFIG_PATH: /tmp/openclaw.json
|
OPENCLAW_CONFIG_PATH: /config/openclaw.json
|
||||||
volumes:
|
volumes:
|
||||||
- mosaic-operations-config:/config:ro
|
- jarvis-operations-config:/config/openclaw.json:ro
|
||||||
- mosaic-operations-state:/home/node/.openclaw
|
- jarvis-operations-state:/home/node/.openclaw
|
||||||
networks:
|
networks:
|
||||||
- mosaic-stack_internal
|
- mosaic-stack_internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -124,7 +120,7 @@ services:
|
|||||||
replicas: 1
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
delay: 10s
|
delay: 5s
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -132,7 +128,7 @@ services:
|
|||||||
reservations:
|
reservations:
|
||||||
memory: 256M
|
memory: 256M
|
||||||
labels:
|
labels:
|
||||||
- com.mosaic.agent=mosaic-operations
|
- com.mosaic.agent=jarvis-operations
|
||||||
- com.mosaic.role=operations
|
- com.mosaic.role=operations
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
@@ -140,11 +136,31 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mosaic-main-config:
|
jarvis-main-config:
|
||||||
mosaic-main-state:
|
driver: local
|
||||||
mosaic-projects-config:
|
driver_opts:
|
||||||
mosaic-projects-state:
|
type: none
|
||||||
mosaic-research-config:
|
o: bind
|
||||||
mosaic-research-state:
|
device: ${PWD}/docker/openclaw-instances/jarvis-main.json
|
||||||
mosaic-operations-config:
|
jarvis-projects-config:
|
||||||
mosaic-operations-state:
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ${PWD}/docker/openclaw-instances/jarvis-projects.json
|
||||||
|
jarvis-research-config:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ${PWD}/docker/openclaw-instances/jarvis-research.json
|
||||||
|
jarvis-operations-config:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ${PWD}/docker/openclaw-instances/jarvis-operations.json
|
||||||
|
jarvis-main-state:
|
||||||
|
jarvis-projects-state:
|
||||||
|
jarvis-research-state:
|
||||||
|
jarvis-operations-state:
|
||||||
|
|||||||
@@ -1,97 +1,47 @@
|
|||||||
# Mosaic Agent Fleet — Setup Guide
|
# OpenClaw Agent Instance Setup
|
||||||
|
|
||||||
## Prerequisites
|
Each service in the OpenClaw fleet reads:
|
||||||
|
|
||||||
- Docker Swarm initialized on target host
|
- A per-agent environment file: `docker/openclaw-instances/<agent>.env`
|
||||||
- Mosaic Stack running (Postgres, Valkey on `mosaic-stack_internal` network)
|
- A per-agent JSON5 config: `docker/openclaw-instances/<agent>.json`
|
||||||
|
|
||||||
## 1. Configure Environment Variables
|
## 1. Fill in API keys in `.env` files
|
||||||
|
|
||||||
Copy and fill in each agent's `.env` file:
|
Set `ZAI_API_KEY` in each instance env file:
|
||||||
|
|
||||||
|
- `jarvis-main.env`
|
||||||
|
- `jarvis-projects.env`
|
||||||
|
- `jarvis-research.env`
|
||||||
|
- `jarvis-operations.env`
|
||||||
|
|
||||||
|
## 2. Generate unique gateway tokens per agent
|
||||||
|
|
||||||
|
Generate one token per instance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd docker/openclaw-instances
|
openssl rand -hex 32
|
||||||
|
|
||||||
# Required for each agent:
|
|
||||||
# ZAI_API_KEY — Your Z.ai API key (or other LLM provider key)
|
|
||||||
# OPENCLAW_GATEWAY_TOKEN — Unique bearer token per agent
|
|
||||||
|
|
||||||
# Generate unique tokens:
|
|
||||||
for agent in main projects research operations; do
|
|
||||||
echo "OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)"
|
|
||||||
done
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional: Local Ollama
|
Set a different `OPENCLAW_GATEWAY_TOKEN` in each `.env` file.
|
||||||
|
|
||||||
If you have an Ollama instance, add to any agent's `.env`:
|
## 3. Deploy the Docker Swarm stack
|
||||||
|
|
||||||
|
From repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OLLAMA_BASE_URL=http://your-ollama-host:11434
|
docker stack deploy -c docker/openclaw-compose.yml jarvis
|
||||||
OLLAMA_MODEL=cogito # or any model you have pulled
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The entrypoint script will automatically inject the Ollama provider at startup.
|
## 4. First-time auth (if needed)
|
||||||
|
|
||||||
### Optional: Override Default Model
|
If an instance requires first-time login, exec into the running container and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
OPENCLAW_MODEL=anthropic/claude-sonnet-4-6
|
openclaw auth
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Populate Config Volumes
|
This uses OpenClaw's headless OAuth device-code flow.
|
||||||
|
|
||||||
Each agent needs its `.json.template` file in its config volume:
|
## 5. Use Mosaic WebUI terminal for auth
|
||||||
|
|
||||||
```bash
|
You can complete the device-code auth flow from the Mosaic WebUI terminal (xterm.js) attached to the service container.
|
||||||
# Create config directories and copy templates
|
|
||||||
for agent in main projects research operations; do
|
|
||||||
mkdir -p /var/lib/docker/volumes/mosaic-agents_mosaic-${agent}-config/_data/
|
|
||||||
cp openclaw-instances/mosaic-${agent}.json.template \
|
|
||||||
/var/lib/docker/volumes/mosaic-agents_mosaic-${agent}-config/_data/openclaw.json.template
|
|
||||||
cp openclaw-instances/entrypoint.sh \
|
|
||||||
/var/lib/docker/volumes/mosaic-agents_mosaic-${agent}-config/_data/entrypoint.sh
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker stack deploy -c docker/openclaw-compose.yml mosaic-agents
|
|
||||||
docker stack services mosaic-agents
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. First-Time Auth (if needed)
|
|
||||||
|
|
||||||
For providers requiring OAuth (e.g., Anthropic):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -it $(docker ps -q -f name=mosaic-main) openclaw auth
|
|
||||||
```
|
|
||||||
|
|
||||||
Follow the device-code flow in your browser. Tokens persist in the state volume.
|
|
||||||
|
|
||||||
You can also use the Mosaic WebUI terminal (xterm.js) for this.
|
|
||||||
|
|
||||||
## 5. Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check health
|
|
||||||
curl http://localhost:18789/health
|
|
||||||
|
|
||||||
# Test chat completions endpoint
|
|
||||||
curl http://localhost:18789/v1/chat/completions \
|
|
||||||
-H "Authorization: Bearer YOUR_GATEWAY_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"model":"openclaw:main","messages":[{"role":"user","content":"hello"}]}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variable Reference
|
|
||||||
|
|
||||||
| Variable | Required | Description |
|
|
||||||
| ------------------------ | -------- | ------------------------------------------------- |
|
|
||||||
| `ZAI_API_KEY` | Yes\* | Z.ai API key (\*or other provider key) |
|
|
||||||
| `OPENCLAW_GATEWAY_TOKEN` | Yes | Bearer token for this agent (unique per instance) |
|
|
||||||
| `OPENCLAW_MODEL` | No | Override default model (default: `zai/glm-5`) |
|
|
||||||
| `OLLAMA_BASE_URL` | No | Ollama endpoint (e.g., `http://10.1.1.42:11434`) |
|
|
||||||
| `OLLAMA_MODEL` | No | Ollama model name (default: `cogito`) |
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Mosaic Agent Fleet — OpenClaw container entrypoint
|
|
||||||
# Renders config template from env vars, optionally adds Ollama provider, starts gateway
|
|
||||||
set -e
|
|
||||||
|
|
||||||
TEMPLATE="/config/openclaw.json.template"
|
|
||||||
CONFIG="/tmp/openclaw.json"
|
|
||||||
|
|
||||||
if [ ! -f "$TEMPLATE" ]; then
|
|
||||||
echo "ERROR: Config template not found at $TEMPLATE"
|
|
||||||
echo "Mount your config volume at /config with a .json.template file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate required env vars
|
|
||||||
: "${OPENCLAW_GATEWAY_TOKEN:?OPENCLAW_GATEWAY_TOKEN is required (generate: openssl rand -hex 32)}"
|
|
||||||
|
|
||||||
# Render template with env var substitution
|
|
||||||
envsubst < "$TEMPLATE" > "$CONFIG"
|
|
||||||
|
|
||||||
# If OLLAMA_BASE_URL is set, inject Ollama provider into config
|
|
||||||
if [ -n "$OLLAMA_BASE_URL" ]; then
|
|
||||||
# Use python3 if available, fall back to node
|
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
|
||||||
python3 -c "
|
|
||||||
import json, sys
|
|
||||||
with open('$CONFIG') as f: cfg = json.load(f)
|
|
||||||
cfg.setdefault('models', {})['mode'] = 'merge'
|
|
||||||
cfg['models'].setdefault('providers', {})['ollama'] = {
|
|
||||||
'baseUrl': '$OLLAMA_BASE_URL/v1',
|
|
||||||
'api': 'openai-completions',
|
|
||||||
'models': [{'id': '${OLLAMA_MODEL:-cogito}', 'name': '${OLLAMA_MODEL:-cogito} (Local)', 'reasoning': False, 'input': ['text'], 'cost': {'input':0,'output':0,'cacheRead':0,'cacheWrite':0}, 'contextWindow': 128000, 'maxTokens': 8192}]
|
|
||||||
}
|
|
||||||
with open('$CONFIG','w') as f: json.dump(cfg, f, indent=2)
|
|
||||||
"
|
|
||||||
echo "Ollama provider added: $OLLAMA_BASE_URL (model: ${OLLAMA_MODEL:-cogito})"
|
|
||||||
elif command -v node >/dev/null 2>&1; then
|
|
||||||
node -e "
|
|
||||||
const fs = require('fs');
|
|
||||||
const cfg = JSON.parse(fs.readFileSync('$CONFIG','utf8'));
|
|
||||||
cfg.models = cfg.models || {}; cfg.models.mode = 'merge';
|
|
||||||
cfg.models.providers = cfg.models.providers || {};
|
|
||||||
cfg.models.providers.ollama = {baseUrl:'$OLLAMA_BASE_URL/v1',api:'openai-completions',models:[{id:'${OLLAMA_MODEL:-cogito}',name:'${OLLAMA_MODEL:-cogito} (Local)',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128000,maxTokens:8192}]};
|
|
||||||
fs.writeFileSync('$CONFIG', JSON.stringify(cfg, null, 2));
|
|
||||||
"
|
|
||||||
echo "Ollama provider added: $OLLAMA_BASE_URL (model: ${OLLAMA_MODEL:-cogito})"
|
|
||||||
else
|
|
||||||
echo "WARNING: OLLAMA_BASE_URL set but no python3/node available to inject provider"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
export OPENCLAW_CONFIG_PATH="$CONFIG"
|
|
||||||
exec openclaw gateway run --bind lan --auth token "$@"
|
|
||||||
3
docker/openclaw-instances/jarvis-main.env
Normal file
3
docker/openclaw-instances/jarvis-main.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
OPENCLAW_CONFIG_PATH=/config/openclaw.json
|
||||||
|
ZAI_API_KEY=REPLACE_WITH_ZAI_API_KEY
|
||||||
|
OPENCLAW_GATEWAY_TOKEN=REPLACE_WITH_UNIQUE_GATEWAY_TOKEN
|
||||||
41
docker/openclaw-instances/jarvis-main.json
Normal file
41
docker/openclaw-instances/jarvis-main.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"mode": "local",
|
||||||
|
"port": 18789,
|
||||||
|
"bind": "lan",
|
||||||
|
"auth": { "mode": "token" },
|
||||||
|
"http": {
|
||||||
|
"endpoints": {
|
||||||
|
"chatCompletions": { "enabled": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "/home/node/workspace",
|
||||||
|
"model": { "primary": "zai/glm-5" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Z.ai is built in and uses ZAI_API_KEY.
|
||||||
|
// Ollama is configured for optional local reasoning fallback.
|
||||||
|
"models": {
|
||||||
|
"mode": "merge",
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://10.1.1.42:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "cogito",
|
||||||
|
"name": "Cogito (Local Reasoning)",
|
||||||
|
"reasoning": false,
|
||||||
|
"input": ["text"],
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
||||||
|
"contextWindow": 128000,
|
||||||
|
"maxTokens": 8192
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
docker/openclaw-instances/jarvis-operations.env
Normal file
3
docker/openclaw-instances/jarvis-operations.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
OPENCLAW_CONFIG_PATH=/config/openclaw.json
|
||||||
|
ZAI_API_KEY=REPLACE_WITH_ZAI_API_KEY
|
||||||
|
OPENCLAW_GATEWAY_TOKEN=REPLACE_WITH_UNIQUE_GATEWAY_TOKEN
|
||||||
40
docker/openclaw-instances/jarvis-operations.json
Normal file
40
docker/openclaw-instances/jarvis-operations.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"mode": "local",
|
||||||
|
"port": 18789,
|
||||||
|
"bind": "lan",
|
||||||
|
"auth": { "mode": "token" },
|
||||||
|
"http": {
|
||||||
|
"endpoints": {
|
||||||
|
"chatCompletions": { "enabled": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "/home/node/workspace",
|
||||||
|
"model": { "primary": "ollama/cogito" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Operations uses local Ollama Cogito as the primary model.
|
||||||
|
"models": {
|
||||||
|
"mode": "merge",
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://10.1.1.42:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "cogito",
|
||||||
|
"name": "Cogito (Local Reasoning)",
|
||||||
|
"reasoning": false,
|
||||||
|
"input": ["text"],
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
||||||
|
"contextWindow": 128000,
|
||||||
|
"maxTokens": 8192
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
docker/openclaw-instances/jarvis-projects.env
Normal file
3
docker/openclaw-instances/jarvis-projects.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
OPENCLAW_CONFIG_PATH=/config/openclaw.json
|
||||||
|
ZAI_API_KEY=REPLACE_WITH_ZAI_API_KEY
|
||||||
|
OPENCLAW_GATEWAY_TOKEN=REPLACE_WITH_UNIQUE_GATEWAY_TOKEN
|
||||||
39
docker/openclaw-instances/jarvis-projects.json
Normal file
39
docker/openclaw-instances/jarvis-projects.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"mode": "local",
|
||||||
|
"port": 18789,
|
||||||
|
"bind": "lan",
|
||||||
|
"auth": { "mode": "token" },
|
||||||
|
"http": {
|
||||||
|
"endpoints": {
|
||||||
|
"chatCompletions": { "enabled": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "/home/node/workspace",
|
||||||
|
"model": { "primary": "zai/glm-5" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"mode": "merge",
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://10.1.1.42:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "cogito",
|
||||||
|
"name": "Cogito (Local Reasoning)",
|
||||||
|
"reasoning": false,
|
||||||
|
"input": ["text"],
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
||||||
|
"contextWindow": 128000,
|
||||||
|
"maxTokens": 8192
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
docker/openclaw-instances/jarvis-research.env
Normal file
3
docker/openclaw-instances/jarvis-research.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
OPENCLAW_CONFIG_PATH=/config/openclaw.json
|
||||||
|
ZAI_API_KEY=REPLACE_WITH_ZAI_API_KEY
|
||||||
|
OPENCLAW_GATEWAY_TOKEN=REPLACE_WITH_UNIQUE_GATEWAY_TOKEN
|
||||||
39
docker/openclaw-instances/jarvis-research.json
Normal file
39
docker/openclaw-instances/jarvis-research.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"gateway": {
|
||||||
|
"mode": "local",
|
||||||
|
"port": 18789,
|
||||||
|
"bind": "lan",
|
||||||
|
"auth": { "mode": "token" },
|
||||||
|
"http": {
|
||||||
|
"endpoints": {
|
||||||
|
"chatCompletions": { "enabled": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "/home/node/workspace",
|
||||||
|
"model": { "primary": "zai/glm-5" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"mode": "merge",
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://10.1.1.42:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "cogito",
|
||||||
|
"name": "Cogito (Local Reasoning)",
|
||||||
|
"reasoning": false,
|
||||||
|
"input": ["text"],
|
||||||
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
||||||
|
"contextWindow": 128000,
|
||||||
|
"maxTokens": 8192
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Mosaic Agent: main
|
|
||||||
# Fill in all values before deploying.
|
|
||||||
|
|
||||||
# Required: LLM provider API key (Z.ai, OpenAI, etc.)
|
|
||||||
ZAI_API_KEY=
|
|
||||||
|
|
||||||
# Required: unique bearer token for this agent instance (generate: openssl rand -hex 32)
|
|
||||||
OPENCLAW_GATEWAY_TOKEN=
|
|
||||||
|
|
||||||
# Optional: override default model (default: zai/glm-5)
|
|
||||||
# OPENCLAW_MODEL=zai/glm-5
|
|
||||||
|
|
||||||
# Optional: Ollama endpoint for local inference (uncomment to enable)
|
|
||||||
# OLLAMA_BASE_URL=
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"gateway": {
|
|
||||||
"mode": "local",
|
|
||||||
"port": 18789,
|
|
||||||
"bind": "lan",
|
|
||||||
"auth": { "mode": "token" },
|
|
||||||
"http": {
|
|
||||||
"endpoints": {
|
|
||||||
"chatCompletions": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"workspace": "/home/node/workspace",
|
|
||||||
"model": { "primary": "${OPENCLAW_MODEL:-zai/glm-5}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Mosaic Agent: operations
|
|
||||||
# Fill in all values before deploying.
|
|
||||||
|
|
||||||
# Required: LLM provider API key (Z.ai, OpenAI, etc.)
|
|
||||||
ZAI_API_KEY=
|
|
||||||
|
|
||||||
# Required: unique bearer token for this agent instance (generate: openssl rand -hex 32)
|
|
||||||
OPENCLAW_GATEWAY_TOKEN=
|
|
||||||
|
|
||||||
# Optional: override default model (default: zai/glm-5)
|
|
||||||
# OPENCLAW_MODEL=zai/glm-5
|
|
||||||
|
|
||||||
# Optional: Ollama endpoint for local inference (uncomment to enable)
|
|
||||||
# OLLAMA_BASE_URL=
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"gateway": {
|
|
||||||
"mode": "local",
|
|
||||||
"port": 18789,
|
|
||||||
"bind": "lan",
|
|
||||||
"auth": { "mode": "token" },
|
|
||||||
"http": {
|
|
||||||
"endpoints": {
|
|
||||||
"chatCompletions": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"workspace": "/home/node/workspace",
|
|
||||||
"model": { "primary": "${OPENCLAW_MODEL:-zai/glm-5}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Mosaic Agent: projects
|
|
||||||
# Fill in all values before deploying.
|
|
||||||
|
|
||||||
# Required: LLM provider API key (Z.ai, OpenAI, etc.)
|
|
||||||
ZAI_API_KEY=
|
|
||||||
|
|
||||||
# Required: unique bearer token for this agent instance (generate: openssl rand -hex 32)
|
|
||||||
OPENCLAW_GATEWAY_TOKEN=
|
|
||||||
|
|
||||||
# Optional: override default model (default: zai/glm-5)
|
|
||||||
# OPENCLAW_MODEL=zai/glm-5
|
|
||||||
|
|
||||||
# Optional: Ollama endpoint for local inference (uncomment to enable)
|
|
||||||
# OLLAMA_BASE_URL=
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"gateway": {
|
|
||||||
"mode": "local",
|
|
||||||
"port": 18789,
|
|
||||||
"bind": "lan",
|
|
||||||
"auth": { "mode": "token" },
|
|
||||||
"http": {
|
|
||||||
"endpoints": {
|
|
||||||
"chatCompletions": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"workspace": "/home/node/workspace",
|
|
||||||
"model": { "primary": "${OPENCLAW_MODEL:-zai/glm-5}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Mosaic Agent: research
|
|
||||||
# Fill in all values before deploying.
|
|
||||||
|
|
||||||
# Required: LLM provider API key (Z.ai, OpenAI, etc.)
|
|
||||||
ZAI_API_KEY=
|
|
||||||
|
|
||||||
# Required: unique bearer token for this agent instance (generate: openssl rand -hex 32)
|
|
||||||
OPENCLAW_GATEWAY_TOKEN=
|
|
||||||
|
|
||||||
# Optional: override default model (default: zai/glm-5)
|
|
||||||
# OPENCLAW_MODEL=zai/glm-5
|
|
||||||
|
|
||||||
# Optional: Ollama endpoint for local inference (uncomment to enable)
|
|
||||||
# OLLAMA_BASE_URL=
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"gateway": {
|
|
||||||
"mode": "local",
|
|
||||||
"port": 18789,
|
|
||||||
"bind": "lan",
|
|
||||||
"auth": { "mode": "token" },
|
|
||||||
"http": {
|
|
||||||
"endpoints": {
|
|
||||||
"chatCompletions": { "enabled": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"agents": {
|
|
||||||
"defaults": {
|
|
||||||
"workspace": "/home/node/workspace",
|
|
||||||
"model": { "primary": "${OPENCLAW_MODEL:-zai/glm-5}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,19 +55,19 @@ Remaining estimate: ~143K tokens (Codex budget).
|
|||||||
|
|
||||||
## MS22 — Fleet Evolution (Phase 0: Knowledge Layer)
|
## MS22 — Fleet Evolution (Phase 0: Knowledge Layer)
|
||||||
|
|
||||||
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
|
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
|
||||||
| --------------- | ------ | ------------ | ------------------------------------------------------------ | -------- | ----- | ------------------------------ | --------------------------------------------------------- | ------------- | ------------ | ---------- | ------------ | -------- | ---- | --------------------------------------------- |
|
| --------------- | ----------- | ------------ | ------------------------------------------------------------ | -------- | ----- | ------------------------------ | --------------------------------------------------------- | ------------- | ------------ | ---------- | ------------ | -------- | ---- | --------------------------------------------- |
|
||||||
| MS22-PLAN-001 | done | p0-knowledge | PRD + mission bootstrap + TASKS.md | TASKS:P0 | stack | feat/ms22-knowledge-schema | — | MS22-DB-001 | orchestrator | 2026-02-28 | 2026-02-28 | 10K | 8K | PRD-MS22.md, mission fleet-evolution-20260228 |
|
| MS22-PLAN-001 | done | p0-knowledge | PRD + mission bootstrap + TASKS.md | TASKS:P0 | stack | feat/ms22-knowledge-schema | — | MS22-DB-001 | orchestrator | 2026-02-28 | 2026-02-28 | 10K | 8K | PRD-MS22.md, mission fleet-evolution-20260228 |
|
||||||
| MS22-DB-001 | done | p0-knowledge | Findings module (pgvector, CRUD, similarity search) | TASKS:P0 | api | feat/ms22-findings | MS22-PLAN-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~22K | PR #585 merged, CI green |
|
| MS22-DB-001 | done | p0-knowledge | Findings module (pgvector, CRUD, similarity search) | TASKS:P0 | api | feat/ms22-findings | MS22-PLAN-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~22K | PR #585 merged, CI green |
|
||||||
| MS22-API-001 | done | p0-knowledge | Findings API endpoints | TASKS:P0 | api | feat/ms22-findings | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-001 |
|
| MS22-API-001 | done | p0-knowledge | Findings API endpoints | TASKS:P0 | api | feat/ms22-findings | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-001 |
|
||||||
| MS22-DB-002 | done | p0-knowledge | AgentMemory module (key/value store, upsert) | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 15K | ~16K | PR #586 merged, CI green |
|
| MS22-DB-002 | done | p0-knowledge | AgentMemory module (key/value store, upsert) | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 15K | ~16K | PR #586 merged, CI green |
|
||||||
| MS22-API-002 | done | p0-knowledge | AgentMemory API endpoints | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-002 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-002 |
|
| MS22-API-002 | done | p0-knowledge | AgentMemory API endpoints | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-002 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-002 |
|
||||||
| MS22-DB-004 | done | p0-knowledge | ConversationArchive module (pgvector, ingest, search) | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~18K | PR #587 merged, CI green |
|
| MS22-DB-004 | done | p0-knowledge | ConversationArchive module (pgvector, ingest, search) | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~18K | PR #587 merged, CI green |
|
||||||
| MS22-API-004 | done | p0-knowledge | ConversationArchive API endpoints | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-004 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-004 |
|
| MS22-API-004 | done | p0-knowledge | ConversationArchive API endpoints | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-004 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-004 |
|
||||||
| MS22-API-005 | done | p0-knowledge | EmbeddingService (reuse existing KnowledgeModule) | TASKS:P0 | api | — | — | — | orchestrator | 2026-02-28 | 2026-02-28 | 0 | 0 | Already existed; no work needed |
|
| MS22-API-005 | done | p0-knowledge | EmbeddingService (reuse existing KnowledgeModule) | TASKS:P0 | api | — | — | — | orchestrator | 2026-02-28 | 2026-02-28 | 0 | 0 | Already existed; no work needed |
|
||||||
| MS22-DB-003 | done | p0-knowledge | Task model: add assigned_agent field + migration | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-001 | MS22-API-003 | — | — | — | 8K | — | Small schema + migration only |
|
| MS22-DB-003 | not-started | p0-knowledge | Task model: add assigned_agent field + migration | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-001 | MS22-API-003 | — | — | — | 8K | — | Small schema + migration only |
|
||||||
| MS22-API-003 | done | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule |
|
| MS22-API-003 | not-started | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule |
|
||||||
| MS22-TEST-001 | done | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres |
|
| MS22-TEST-001 | not-started | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres |
|
||||||
| MS22-SKILL-001 | done | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ |
|
| MS22-SKILL-001 | not-started | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ |
|
||||||
| MS22-INGEST-001 | done | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs |
|
| MS22-INGEST-001 | done | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs |
|
||||||
| MS22-VER-P0 | done | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |
|
| MS22-VER-P0 | not-started | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |
|
||||||
|
|||||||
Reference in New Issue
Block a user