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`:
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue