mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
* MUL-3284 PR3 (CLI): multica runtime profile subcommands + local path override
- cmd_runtime_profile.go: `multica runtime profile` group — list / create /
update / delete against /api/workspaces/{id}/runtime-profiles, plus set-path
/ unset-path for a per-machine command override. protocol-family validated
client-side via agent.IsSupportedType / agent.SupportedTypes; visibility
validated; update only sends changed flags (protocol_family immutable);
delete surfaces the server 409 body when agents are still bound.
- internal/cli/config.go: ProfileCommandOverrides map[string]string on
CLIConfig (omitempty), through the existing marshal/unmarshal so set/unset
round-trips without dropping other fields.
- internal/daemon: Config.ProfileCommandOverrides, loaded from CLIConfig;
appendProfileRuntimes now prefers an override path when set AND executable,
else falls back to exec.LookPath(command_name), else skips+logs as before.
- Tests: cmd_runtime_profile_test.go (registration, create/update/delete incl.
bad-family + missing-flag + 409 surfacing, set/unset path round-trip,
relative-path rejection, config preservation); cli/config round-trip;
daemon prefers-override / falls-back-when-not-executable.
Verified: go build ./..., go vet, go test ./cmd/multica/... ./internal/daemon/...
./internal/cli/... all pass.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3 (Web): custom runtime profiles in the Runtime page
Single-list integration — no new page, no tabs/grouping. Built-in protocol
families and custom profiles render mixed in one catalog, each row badged
built-in vs custom (progressive disclosure).
- packages/core: RUNTIME_PROFILE_PROTOCOL_FAMILIES (single-source 13-family
whitelist, matches server agent.SupportedTypes + migration 120 CHECK) and
RuntimeProtocolFamily / RuntimeProfile types; api client
list/get/create/update/deleteRuntimeProfile against
/api/workspaces/{id}/runtime-profiles; runtimes/profiles.ts query +
mutation hooks and a 409 "agents still bound" conflict parser.
- packages/views/runtimes: runtime-profile-catalog (mixed built-in+custom
rows), runtime-profiles-dialog (header "+ Add runtime" → step 1 pick
protocol family → step 2 display_name/command_name/description; edit form
for custom; admin-gated), delete-runtime-profile-dialog (confirm + graceful
409), runtimes-page / runtime-list integration.
- i18n: new strings added to all four locales (en, zh-Hans, ja, ko).
- a11y: dialogs are focus-trapped, Esc-closable, labelled; full
create/edit/delete flow is keyboard + screen-reader operable.
Iron rule honored: no generic per-agent args UI here (those stay on Agent
config). fixed_args is not surfaced as a general args field.
Verified: turbo typecheck + lint + test pass for @multica/core, @multica/views,
@multica/web; the @multica/web production build succeeds.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: hide fixed_args from Web + CLI (not yet wired to launch)
Review fix. fixed_args was surfaced as a working feature, but the daemon does
not splice it into the agent launch command — exposing it promised admins a
no-op. Per the call, remove it from every user-facing surface while keeping the
underlying column/struct "carried but not exposed".
- Web (runtime-profiles-dialog.tsx + runtime-profile-catalog.ts): drop the
detail row, the create body field, the update patch field, and the form
textarea; remove the parseFixedArgs/fixedArgsToText helpers and the
fixedArgs form value. Left a NOTE pointing at the daemon TODO.
- i18n: removed the fixed_args strings from all four locales (en/zh-Hans/ja/ko).
- CLI (cmd_runtime_profile.go): removed the `--fixed-arg` flag from create and
update and stopped sending `fixed_args`; updated the "no fields" message.
Test now asserts the CLI never sends fixed_args.
Untouched (the carried-but-not-exposed layer): the runtime_profile.fixed_args
column, the server handler's accept/return, and the daemon's RuntimeProfile
field — all keep the existing TODO(MUL-3284) to wire it into the launch path
(with a test proving args reach the backend) before any UI/CLI re-exposes it.
Verified: turbo typecheck+lint+test pass for @multica/core and @multica/views;
go build/vet/test pass for ./cmd/multica/.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: stop exposing profile visibility=private (server forces workspace)
Double-review (Eve) caught a fixed_args-shaped hole: visibility=private was a
user-facing toggle (Web form + detail + CLI), but the three server read paths
(ListRuntimeProfiles, daemon ListEnabledRuntimeProfilesForWorkspace,
DaemonRegister) never enforce it — so a "private" profile's name/command would
leak to other members and could be registered by other machines' daemons
(lateral data leak). Same "don't paint a pie" fix as fixed_args: hide the
control everywhere and force the stored value.
- Server (runtime_profile.go): drop `visibility` from the create + update
request structs; CreateRuntimeProfile always stores 'workspace'
(runtimeProfileDefaultVisibility); UpdateRuntimeProfile no longer accepts it;
removed validRuntimeProfileVisibility. The column + response field stay
(always 'workspace') as the carried-but-not-exposed layer.
- Web (runtime-profiles-dialog.tsx): removed the visibility form fieldset,
the VisibilityOption component, the detail row, the visibility state, and the
create/update submit fields.
- i18n: removed the profile visibility strings from all four locales
(profiles.detail.visibility, profiles.visibility.*, profiles.form.visibility_*).
Top-level runtime/agent visibility strings are untouched.
- CLI (cmd_runtime_profile.go): removed `--visibility` from create/update and
the VISIBILITY list column; removed validateVisibility; stopped sending the
field.
- Tests: new TestCreateRuntimeProfile_ForcesWorkspaceVisibility (POST
visibility:"private" -> response and DB row are 'workspace'); CLI create test
now asserts visibility is never sent.
Follow-up MUL-3308 tracks implementing real creator-visibility (and wiring
fixed_args to the launch path); TODOs left in server/Web/CLI point to it.
Verified: turbo typecheck+lint+test pass (@multica/core, @multica/views);
go build/vet pass; go test ./cmd/multica/... and the full ./internal/handler/
suite pass against a migrated Postgres 17.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
import { queryOptions, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "../api";
|
|
import { ApiError } from "../api";
|
|
import type {
|
|
CreateRuntimeProfileRequest,
|
|
RuntimeProfile,
|
|
UpdateRuntimeProfileRequest,
|
|
} from "../types/agent";
|
|
import { runtimeKeys } from "./queries";
|
|
|
|
// Query keys for the workspace-scoped custom runtime profile catalog. Kept
|
|
// separate from `runtimeKeys` (which key the registered runtime *instances*)
|
|
// because the two resources invalidate on different events — but a profile
|
|
// delete can archive bound agents and therefore must also invalidate the
|
|
// instance list, so the mutations below touch both.
|
|
export const runtimeProfileKeys = {
|
|
all: (wsId: string) => ["runtime-profiles", wsId] as const,
|
|
list: (wsId: string) => [...runtimeProfileKeys.all(wsId), "list"] as const,
|
|
detail: (wsId: string, profileId: string) =>
|
|
[...runtimeProfileKeys.all(wsId), "detail", profileId] as const,
|
|
};
|
|
|
|
export function runtimeProfileListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: runtimeProfileKeys.list(wsId),
|
|
queryFn: () => api.listRuntimeProfiles(wsId),
|
|
});
|
|
}
|
|
|
|
export function useCreateRuntimeProfile(wsId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: CreateRuntimeProfileRequest) =>
|
|
api.createRuntimeProfile(wsId, body),
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: runtimeProfileKeys.all(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateRuntimeProfile(wsId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({
|
|
profileId,
|
|
patch,
|
|
}: {
|
|
profileId: string;
|
|
patch: UpdateRuntimeProfileRequest;
|
|
}) => api.updateRuntimeProfile(wsId, profileId, patch),
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: runtimeProfileKeys.all(wsId) });
|
|
// A rename / visibility change can affect how the runtime list
|
|
// labels bound instances; refresh that too.
|
|
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteRuntimeProfile(wsId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (profileId: string) => api.deleteRuntimeProfile(wsId, profileId),
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: runtimeProfileKeys.all(wsId) });
|
|
// The strict DELETE refuses (409) while agents are bound, but once it
|
|
// succeeds the bound-instance picture may change — keep the runtime
|
|
// list in sync.
|
|
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
// The server returns a 409 with a machine-readable code when a delete is
|
|
// refused because active agents are still bound to the profile. We surface
|
|
// the server's human-readable message verbatim so the confirm dialog can
|
|
// explain the refusal without re-deriving it. Non-409s and unrelated codes
|
|
// collapse to `null` so callers fall through to the generic error path.
|
|
export interface RuntimeProfileBoundConflict {
|
|
message: string;
|
|
}
|
|
|
|
export function parseRuntimeProfileBoundConflict(
|
|
err: unknown,
|
|
): RuntimeProfileBoundConflict | null {
|
|
if (!(err instanceof ApiError)) return null;
|
|
if (err.status !== 409) return null;
|
|
const body = err.body;
|
|
const fallback = err.message;
|
|
if (body && typeof body === "object") {
|
|
const record = body as Record<string, unknown>;
|
|
const message =
|
|
typeof record.message === "string" && record.message.trim()
|
|
? record.message
|
|
: typeof record.error === "string" && record.error.trim()
|
|
? record.error
|
|
: fallback;
|
|
return { message };
|
|
}
|
|
return { message: fallback };
|
|
}
|
|
|
|
export type { RuntimeProfile };
|