refactor: move Docker files to docker/ and CHANGELOG to docs/

This commit is contained in:
iliya 2026-02-27 21:04:11 +02:00
parent 2f56cf4cf2
commit 5b08cca69b
8 changed files with 62 additions and 21 deletions

4
.github/SECURITY.md vendored
View file

@ -26,11 +26,11 @@ In standalone mode (Docker or `node dist-standalone/index.cjs`), the auto-update
For maximum trust, run the Docker container with `--network none`: For maximum trust, run the Docker container with `--network none`:
```bash ```bash
docker build -t claude-agent-teams-ui . docker build -t claude-agent-teams-ui -f docker/Dockerfile .
docker run --network none -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-agent-teams-ui docker run --network none -p 3456:3456 -v ~/.claude:/data/.claude:ro claude-agent-teams-ui
``` ```
Or with Docker Compose, uncomment `network_mode: "none"` in `docker-compose.yml`. Or with Docker Compose, uncomment `network_mode: "none"` in `docker/docker-compose.yml`.
## IPC & Input Validation ## IPC & Input Validation

View file

@ -2,7 +2,7 @@
# Claude Agent Teams UI — Docker Compose # Claude Agent Teams UI — Docker Compose
# #
# Quick start: # Quick start:
# docker compose up # docker compose -f docker/docker-compose.yml up
# #
# Then open http://localhost:3456 in your browser. # Then open http://localhost:3456 in your browser.
# #
@ -14,7 +14,9 @@
services: services:
claude-agent-teams-ui: claude-agent-teams-ui:
build: . build:
context: ..
dockerfile: docker/Dockerfile
ports: ports:
- "3456:3456" - "3456:3456"
volumes: volumes:

View file

@ -413,6 +413,7 @@ function sendInboxMessage(paths, teamName, flags) {
: String(Date.now()) + '-' + String(Math.random()); : String(Date.now()) + '-' + String(Math.random());
const payload = { const payload = {
from, from,
to,
text, text,
timestamp: nowIso(), timestamp: nowIso(),
read: false, read: false,

View file

@ -15,6 +15,7 @@ export class TeamInboxWriter {
const payload: InboxMessage = { const payload: InboxMessage = {
from: request.from ?? 'user', from: request.from ?? 'user',
to: request.member,
text: request.text, text: request.text,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
read: false, read: false,

View file

@ -142,6 +142,28 @@ function buildMembers(members: MemberDraft[]): TeamCreateRequest['members'] {
.filter((member): member is NonNullable<typeof member> => member !== null); .filter((member): member is NonNullable<typeof member> => member !== null);
} }
// eslint-disable-next-line security/detect-unsafe-regex -- kebab-case pattern is linear, no ReDoS
const TEAM_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const MEMBER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
function validateTeamNameInline(name: string): string | null {
const trimmed = name.trim();
if (!trimmed) return null;
if (!TEAM_NAME_RE.test(trimmed) || trimmed.length > 64) {
return 'Use kebab-case [a-z0-9-], max 64 chars';
}
return null;
}
function validateMemberNameInline(name: string): string | null {
const trimmed = name.trim();
if (!trimmed) return null;
if (!MEMBER_NAME_RE.test(trimmed)) {
return 'Start with alphanumeric, use only [a-zA-Z0-9._-], max 128 chars';
}
return null;
}
function validateRequest( function validateRequest(
request: TeamCreateRequest, request: TeamCreateRequest,
options?: { requireCwd?: boolean } options?: { requireCwd?: boolean }
@ -692,6 +714,8 @@ export const CreateTeamDialog = ({
/> />
{existingTeamNames.includes(teamName.trim()) ? ( {existingTeamNames.includes(teamName.trim()) ? (
<p className="text-[11px] text-red-300">Team name already exists</p> <p className="text-[11px] text-red-300">Team name already exists</p>
) : validateTeamNameInline(teamName) ? (
<p className="text-[11px] text-red-300">{validateTeamNameInline(teamName)}</p>
) : fieldErrors.teamName ? ( ) : fieldErrors.teamName ? (
<p className="text-[11px] text-red-300">{fieldErrors.teamName}</p> <p className="text-[11px] text-red-300">{fieldErrors.teamName}</p>
) : null} ) : null}
@ -727,20 +751,27 @@ export const CreateTeamDialog = ({
borderLeftColor: memberColorSet.border, borderLeftColor: memberColorSet.border,
}} }}
> >
<Input <div className="space-y-0.5">
className="h-8 text-xs" <Input
value={member.name} className="h-8 text-xs"
aria-label={`Member ${index + 1} name`} value={member.name}
onChange={(event) => updateMemberName(member.id, event.target.value)} aria-label={`Member ${index + 1} name`}
placeholder="member-name" onChange={(event) => updateMemberName(member.id, event.target.value)}
style={ placeholder="member-name"
member.name.trim() style={
? { member.name.trim()
color: memberColorSet.text, ? {
} color: memberColorSet.text,
: undefined }
} : undefined
/> }
/>
{validateMemberNameInline(member.name) ? (
<p className="text-[10px] text-red-300">
{validateMemberNameInline(member.name)}
</p>
) : null}
</div>
<div className="space-y-1"> <div className="space-y-1">
<Select <Select
value={member.roleSelection || NO_ROLE} value={member.roleSelection || NO_ROLE}
@ -791,9 +822,15 @@ export const CreateTeamDialog = ({
<MembersJsonEditor value={jsonText} onChange={handleJsonChange} error={jsonError} /> <MembersJsonEditor value={jsonText} onChange={handleJsonChange} error={jsonError} />
) : null} ) : null}
</div> </div>
{fieldErrors.members ? ( {(() => {
<p className="text-[11px] text-red-300">{fieldErrors.members}</p> const names = members.map((m) => m.name.trim().toLowerCase()).filter(Boolean);
) : null} const hasDuplicates = new Set(names).size !== names.length;
if (hasDuplicates)
return <p className="text-[11px] text-red-300">Member names must be unique</p>;
if (fieldErrors.members)
return <p className="text-[11px] text-red-300">{fieldErrors.members}</p>;
return null;
})()}
</div> </div>
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-4 md:col-span-2"> <div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-4 md:col-span-2">