refactor: move Docker files to docker/ and CHANGELOG to docs/
This commit is contained in:
parent
2f56cf4cf2
commit
5b08cca69b
8 changed files with 62 additions and 21 deletions
4
.github/SECURITY.md
vendored
4
.github/SECURITY.md
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -413,6 +413,7 @@ function sendInboxMessage(paths, teamName, flags) {
|
|||
: String(Date.now()) + '-' + String(Math.random());
|
||||
const payload = {
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
timestamp: nowIso(),
|
||||
read: false,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue