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`:
```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
```
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

View file

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

View file

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

View file

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

View file

@ -142,6 +142,28 @@ function buildMembers(members: MemberDraft[]): TeamCreateRequest['members'] {
.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(
request: TeamCreateRequest,
options?: { requireCwd?: boolean }
@ -692,6 +714,8 @@ export const CreateTeamDialog = ({
/>
{existingTeamNames.includes(teamName.trim()) ? (
<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 ? (
<p className="text-[11px] text-red-300">{fieldErrors.teamName}</p>
) : null}
@ -727,20 +751,27 @@ export const CreateTeamDialog = ({
borderLeftColor: memberColorSet.border,
}}
>
<Input
className="h-8 text-xs"
value={member.name}
aria-label={`Member ${index + 1} name`}
onChange={(event) => updateMemberName(member.id, event.target.value)}
placeholder="member-name"
style={
member.name.trim()
? {
color: memberColorSet.text,
}
: undefined
}
/>
<div className="space-y-0.5">
<Input
className="h-8 text-xs"
value={member.name}
aria-label={`Member ${index + 1} name`}
onChange={(event) => updateMemberName(member.id, event.target.value)}
placeholder="member-name"
style={
member.name.trim()
? {
color: memberColorSet.text,
}
: undefined
}
/>
{validateMemberNameInline(member.name) ? (
<p className="text-[10px] text-red-300">
{validateMemberNameInline(member.name)}
</p>
) : null}
</div>
<div className="space-y-1">
<Select
value={member.roleSelection || NO_ROLE}
@ -791,9 +822,15 @@ export const CreateTeamDialog = ({
<MembersJsonEditor value={jsonText} onChange={handleJsonChange} error={jsonError} />
) : null}
</div>
{fieldErrors.members ? (
<p className="text-[11px] text-red-300">{fieldErrors.members}</p>
) : null}
{(() => {
const names = members.map((m) => m.name.trim().toLowerCase()).filter(Boolean);
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 className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-4 md:col-span-2">