@worca/app 1.4.0 → 1.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/docker/.env.example +54 -0
- package/docker/compose.clonein.yml +24 -0
- package/docker/compose.dev.yml +24 -0
- package/docker/compose.egress.yml +47 -0
- package/docker/compose.ssh.yml +17 -0
- package/docker/compose.teams.yml +25 -0
- package/docker/compose.yml +57 -0
- package/package.json +4 -1
- package/src/cli/container.mjs +227 -0
- package/src/cli/models.mjs +5 -4
- package/src/cli/schedule.mjs +23 -10
- package/src/cli/worca-cc.mjs +62 -4
- package/src/core/agent-user.mjs +92 -0
- package/src/core/artifacts.mjs +41 -9
- package/src/core/ask/clone-deps.mjs +25 -0
- package/src/core/ask/clone-proposal.mjs +79 -0
- package/src/core/ask/events.mjs +10 -0
- package/src/core/ask/mcp-stdio.mjs +4 -2
- package/src/core/ask/model-proposal.mjs +14 -3
- package/src/core/ask/prompt.mjs +27 -6
- package/src/core/ask/schedule-deps.mjs +16 -11
- package/src/core/ask/spawn.mjs +8 -3
- package/src/core/ask/store.mjs +40 -10
- package/src/core/ask/tool-deps.mjs +36 -1
- package/src/core/ask/tools.mjs +69 -8
- package/src/core/ask/turn.mjs +31 -1
- package/src/core/bridge/errors.mjs +50 -2
- package/src/core/bridge/provider-ops.mjs +20 -7
- package/src/core/bridge/providers/copilot.mjs +51 -5
- package/src/core/bridge/translate/common.mjs +144 -0
- package/src/core/bridge/translate/request.mjs +16 -73
- package/src/core/bridge/translate/responses-request.mjs +185 -0
- package/src/core/bridge/translate/responses-stream.mjs +327 -0
- package/src/core/bridge/upstream.mjs +37 -10
- package/src/core/cf-access.mjs +90 -0
- package/src/core/chat/command-router.mjs +17 -12
- package/src/core/claude-runner.mjs +26 -3
- package/src/core/clone-project.mjs +139 -0
- package/src/core/config.mjs +16 -9
- package/src/core/db.mjs +60 -5
- package/src/core/deployment.mjs +33 -0
- package/src/core/diff-comments.mjs +17 -8
- package/src/core/git-info.mjs +27 -7
- package/src/core/github-app.mjs +107 -0
- package/src/core/github-credentials.mjs +93 -0
- package/src/core/graph/executor.mjs +22 -4
- package/src/core/graph/script-runner.mjs +24 -3
- package/src/core/guardrails.mjs +1 -0
- package/src/core/identity.mjs +83 -0
- package/src/core/metrics/pr-events-workflow.yml +127 -0
- package/src/core/metrics/prs.mjs +383 -0
- package/src/core/metrics/read.mjs +39 -29
- package/src/core/metrics/record.mjs +30 -2
- package/src/core/metrics/sync.mjs +35 -4
- package/src/core/model-env.mjs +47 -7
- package/src/core/model-test.mjs +29 -5
- package/src/core/notifications.mjs +36 -14
- package/src/core/orchestrator.mjs +10 -4
- package/src/core/phases.mjs +2 -0
- package/src/core/pipeline-delete.mjs +4 -3
- package/src/core/plugin-repo.mjs +7 -3
- package/src/core/policy/gate.mjs +9 -5
- package/src/core/policy/state.mjs +2 -2
- package/src/core/policy/sync.mjs +5 -4
- package/src/core/remote-access.mjs +138 -0
- package/src/core/run-harness.mjs +71 -9
- package/src/core/scheduler.mjs +63 -33
- package/src/shared/team-metrics/timeline.mjs +330 -0
- package/ui/public/app.js +591 -14
- package/ui/public/ask-panel.mjs +65 -3
- package/ui/public/bridge-view.mjs +72 -14
- package/ui/public/index.html +42 -9
- package/ui/public/schedules-view.mjs +21 -2
- package/ui/public/session-guard.mjs +77 -0
- package/ui/public/style.css +171 -1
- package/ui/public/team-metrics-timeline.mjs +480 -0
- package/ui/public/team-metrics-view.mjs +10 -2
- package/ui/public/team-policy-view.mjs +6 -1
- package/ui/server.mjs +454 -104
package/README.md
CHANGED
|
@@ -197,6 +197,11 @@ and durations, the clarify Q&A, agent transcripts, and logs:
|
|
|
197
197
|
another project (or a workspace's metrics home) that already records.
|
|
198
198
|
- **A Team metrics page** — project and workspace scope, spend/runs/duration/autonomy/review
|
|
199
199
|
KPIs, breakdowns and a CSV export.
|
|
200
|
+
- **A Timeline for planners** — work items as bars on a calendar (month → week → day), grouped
|
|
201
|
+
by work item or person: what shipped (the PR merged), what waits for review, what needs
|
|
202
|
+
attention. Merge dates come from an optional GitHub Action (`worca metrics pr-workflow`) or the
|
|
203
|
+
GitHub CLI, and the page still works without either. With the Action, pull requests made
|
|
204
|
+
outside Worca show too, so the calendar covers the whole team's delivery.
|
|
200
205
|
- **`worca metrics push`** — flush pending run records from the CLI, e.g. on a headless machine.
|
|
201
206
|
|
|
202
207
|
See [`docs/team-metrics.md`](docs/team-metrics.md).
|
|
@@ -238,7 +243,7 @@ See [`docs/team-policy.md`](docs/team-policy.md).
|
|
|
238
243
|
in-process bridge lets the Claude Code CLI run against a Copilot subscription
|
|
239
244
|
(Claude models through Copilot's native Anthropic endpoint, thinking intact;
|
|
240
245
|
GPT, Gemini and the rest through a translation layer) or any
|
|
241
|
-
`/chat/completions` endpoint — no LiteLLM, no second daemon. Sign in once on
|
|
246
|
+
`/chat/completions` or `/responses` endpoint — no LiteLLM, no second daemon. Sign in once on
|
|
242
247
|
Settings › Models › Providers, import Copilot's models, pick them anywhere.
|
|
243
248
|
See [`docs/models.md`](docs/models.md).
|
|
244
249
|
|
|
@@ -270,6 +275,31 @@ Requirements:
|
|
|
270
275
|
`postinstall` didn't run) or the layout isn't npm's, Worca says so rather than
|
|
271
276
|
a bare `ENOENT`; `WORCA_CLAUDE_BIN` can always point at a `claude.exe` directly.
|
|
272
277
|
|
|
278
|
+
### In a container
|
|
279
|
+
|
|
280
|
+
Prefer the agents to run in a disposable Linux box instead of on your machine?
|
|
281
|
+
The same Worca ships as an image (`ghcr.io/sinishadjukic/worca`) with a Compose
|
|
282
|
+
file; the UI, the CLI, guardrails, plugins and your data work the same way.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
worca container up # from the npm install: writes ~/.worca-cc/container/, starts the box
|
|
286
|
+
worca container login # log Claude Code in, once; UI on http://localhost:4317
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
or, without npm, download `docker/compose.yml`, set `WORCA_PROJECTS` in a
|
|
290
|
+
`.env` beside it and `docker compose up -d`. See [`docs/docker.md`](docs/docker.md)
|
|
291
|
+
for login options, git credentials, the egress allowlist, clone-in mode and
|
|
292
|
+
the Windows/WSL2 notes.
|
|
293
|
+
|
|
294
|
+
### Hosted, behind Cloudflare Access
|
|
295
|
+
|
|
296
|
+
The same image runs as an always-on service: on Railway (or any host), reachable only through a
|
|
297
|
+
Cloudflare Tunnel with Cloudflare Access in front, and worca verifying the Access token itself.
|
|
298
|
+
Step by step: [`docs/deploy-railway.md`](docs/deploy-railway.md); the Cloudflare side and the
|
|
299
|
+
security model: [`docs/remote-access.md`](docs/remote-access.md). Upgrades, configuration and checks of a running
|
|
300
|
+
deployment: [Operate your deployment](docs/deploy-railway.md#operate-your-deployment), with the
|
|
301
|
+
`tools/railway/worca-railway.mjs` tool and the `/worca-railway` skill for Claude Code.
|
|
302
|
+
|
|
273
303
|
## Quick start
|
|
274
304
|
|
|
275
305
|
### Web UI
|
|
@@ -334,6 +364,9 @@ worca --project /path/to/your/project --prompt "demo task" --mock --yes
|
|
|
334
364
|
# flush pending team-metrics run records (headless machines with no UI server)
|
|
335
365
|
worca metrics push
|
|
336
366
|
|
|
367
|
+
# record PR merges for the Team metrics Timeline (adds a GitHub Action; commit and push it)
|
|
368
|
+
worca metrics pr-workflow
|
|
369
|
+
|
|
337
370
|
# team policy: what applies to this project, fetch the branch now, meet the setup checklist
|
|
338
371
|
worca policy show
|
|
339
372
|
worca policy pull
|
|
@@ -387,6 +420,8 @@ The skill starts the same deterministic orchestrator.
|
|
|
387
420
|
- [Models](docs/models.md) — the catalog, providers (GitHub Copilot, OpenAI-compatible) and the built-in bridge
|
|
388
421
|
- [Getting started](docs/getting-started.md) — the in-app checklist, welcome and spotlight guides
|
|
389
422
|
- [Storage](docs/storage.md) — where state lives, project keys, migration
|
|
423
|
+
- [Remote access](docs/remote-access.md) — opt-in, behind Cloudflare Access, with worca checking the token
|
|
424
|
+
- [Deploy on Railway](docs/deploy-railway.md) — the container as a hosted service behind Cloudflare Access
|
|
390
425
|
- [Releasing](docs/RELEASING.md) — how `@worca/app` versions are published
|
|
391
426
|
- [Contributing](CONTRIBUTING.md) — developing Worca from source
|
|
392
427
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# docker/.env.example — copy to .env next to compose.yml. Every line is optional.
|
|
2
|
+
# chmod 600 .env if it holds a token.
|
|
3
|
+
|
|
4
|
+
# The folder that holds your repositories (mounted at the same path inside;
|
|
5
|
+
# see docs/docker.md for the Windows/WSL2 and /projects notes).
|
|
6
|
+
WORCA_PROJECTS=/Users/me/dev
|
|
7
|
+
# Mount it somewhere else inside the container (loses path parity):
|
|
8
|
+
#WORCA_PROJECTS_MOUNT=/projects
|
|
9
|
+
|
|
10
|
+
# Image tag: latest | rc | 1.3.0 | 1.3.0-full | dev
|
|
11
|
+
#WORCA_TAG=latest
|
|
12
|
+
|
|
13
|
+
# Host port the UI is published on (loopback only).
|
|
14
|
+
#WORCA_PORT=4317
|
|
15
|
+
|
|
16
|
+
# Scheduled runs use wall-clock time in this zone.
|
|
17
|
+
TZ=Europe/Berlin
|
|
18
|
+
|
|
19
|
+
# Git identity for the commits agents make in run worktrees.
|
|
20
|
+
GIT_AUTHOR_NAME=Your Name
|
|
21
|
+
GIT_AUTHOR_EMAIL=you@example.com
|
|
22
|
+
|
|
23
|
+
# GitHub over HTTPS: PRs, GitHub Issues source, team metrics/policy branches, clone-in.
|
|
24
|
+
#GH_TOKEN=github_pat_...
|
|
25
|
+
|
|
26
|
+
# Claude Code auth. Prefer none of these and `docker compose run --rm worca claude`
|
|
27
|
+
# to log in once into the volume. Otherwise ONE of:
|
|
28
|
+
#CLAUDE_CODE_OAUTH_TOKEN=... # from `claude setup-token` on a logged-in machine
|
|
29
|
+
#ANTHROPIC_API_KEY=sk-ant-... # Console key (visible to every child process)
|
|
30
|
+
# API key as a compose secret instead (never in the environment):
|
|
31
|
+
# put the key in ./anthropic_api_key, then add to a compose.override.yml:
|
|
32
|
+
# services: { worca: { secrets: [anthropic_api_key] } }
|
|
33
|
+
# secrets: { anthropic_api_key: { file: ./anthropic_api_key } }
|
|
34
|
+
|
|
35
|
+
# Linux Engine only: your uid/gid when not 1000 (`id -u`, `id -g`).
|
|
36
|
+
#WORCA_UID=1000
|
|
37
|
+
#WORCA_GID=1000
|
|
38
|
+
|
|
39
|
+
# Resource caps.
|
|
40
|
+
#WORCA_MEM=6g
|
|
41
|
+
#WORCA_CPUS=4
|
|
42
|
+
|
|
43
|
+
# Corporate proxy / TLS interception.
|
|
44
|
+
#HTTPS_PROXY=http://proxy.corp:3128
|
|
45
|
+
#NO_PROXY=127.0.0.1,localhost
|
|
46
|
+
|
|
47
|
+
# Egress overlay (compose.egress.yml): allowlist, ".host" allows subdomains.
|
|
48
|
+
#WORCA_EGRESS_ALLOW=api.anthropic.com,github.com,.github.com,.githubusercontent.com,registry.npmjs.org
|
|
49
|
+
|
|
50
|
+
# SSH overlay (compose.ssh.yml), Linux Engine / Podman only.
|
|
51
|
+
#WORCA_SSH_SOCK=/run/user/1000/keyring/ssh
|
|
52
|
+
|
|
53
|
+
# Teams overlay (compose.teams.yml).
|
|
54
|
+
#CLOUDFLARE_TUNNEL_TOKEN=...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# docker/compose.clonein.yml — clone-in mode: nothing from the host is mounted
|
|
2
|
+
# (docs/docker.md, "Clone-in mode").
|
|
3
|
+
#
|
|
4
|
+
# docker compose -f compose.yml -f compose.clonein.yml up -d
|
|
5
|
+
# docker compose -f compose.yml -f compose.clonein.yml run --rm worca \
|
|
6
|
+
# git clone https://github.com/acme/api.git /projects/api
|
|
7
|
+
# docker compose -f compose.yml -f compose.clonein.yml run --rm worca worca add --path /projects/api
|
|
8
|
+
#
|
|
9
|
+
# /projects becomes a named volume. Results leave the container only as pushed
|
|
10
|
+
# branches and PRs, so git credentials (GH_TOKEN or the ssh overlay) are needed.
|
|
11
|
+
# `!override` REPLACES the base file's volume list (compose merges lists by
|
|
12
|
+
# target otherwise, which would keep the host bind mount).
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
worca:
|
|
16
|
+
volumes: !override
|
|
17
|
+
- worca-home:/worca
|
|
18
|
+
- claude-config:/home/worca/.claude
|
|
19
|
+
- projects:/projects
|
|
20
|
+
environment:
|
|
21
|
+
WORCA_PROJECTS_ROOT: /projects
|
|
22
|
+
|
|
23
|
+
volumes:
|
|
24
|
+
projects:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# docker/compose.dev.yml — hack on Worca itself inside the container (CONTRIBUTING.md).
|
|
2
|
+
#
|
|
3
|
+
# cd docker
|
|
4
|
+
# docker compose -f compose.yml -f compose.dev.yml up # server from ../ (this checkout)
|
|
5
|
+
# docker compose -f compose.yml -f compose.dev.yml run --rm worca npm test
|
|
6
|
+
#
|
|
7
|
+
# Bind-mounts the repository checkout over /app and runs the server from it,
|
|
8
|
+
# so edits on the host are live in the container's Linux `claude`. node_modules
|
|
9
|
+
# is a container-side tmpfs owned by the worca user (a named volume at a path
|
|
10
|
+
# the image does not have would be created root-owned): the host's modules stay
|
|
11
|
+
# untouched, and `npm ci` (about a second) runs on every start.
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
worca:
|
|
15
|
+
image: ghcr.io/sinishadjukic/worca:${WORCA_TAG:-dev}
|
|
16
|
+
working_dir: /app
|
|
17
|
+
volumes:
|
|
18
|
+
- ..:/app
|
|
19
|
+
tmpfs:
|
|
20
|
+
- /tmp:size=2g
|
|
21
|
+
- /app/node_modules:size=1g,uid=1000,gid=1000
|
|
22
|
+
environment:
|
|
23
|
+
WORCA_HOME: /worca
|
|
24
|
+
command: ["bash", "-c", "npm ci --no-audit --no-fund && exec node --disable-warning=ExperimentalWarning ui/server.mjs"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# docker/compose.egress.yml — egress allowlist overlay (docs/docker.md, "Egress allowlist").
|
|
2
|
+
#
|
|
3
|
+
# docker compose -f compose.yml -f compose.egress.yml up -d
|
|
4
|
+
#
|
|
5
|
+
# Puts worca on an INTERNAL network (no route to the internet) and adds a
|
|
6
|
+
# forward proxy from the same image that relays only to allowlisted hosts.
|
|
7
|
+
# Tools that honour HTTPS_PROXY (claude, git, gh, npm, pip, curl) work for
|
|
8
|
+
# allowed hosts; a raw socket from `node -e` or `python -c` cannot leave.
|
|
9
|
+
#
|
|
10
|
+
# WORCA_EGRESS_ALLOW comma-separated hosts; ".example.com" allows subdomains.
|
|
11
|
+
# Default: api.anthropic.com, github.com, .github.com,
|
|
12
|
+
# .githubusercontent.com, registry.npmjs.org
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
worca:
|
|
16
|
+
depends_on:
|
|
17
|
+
- egress
|
|
18
|
+
networks:
|
|
19
|
+
- internal
|
|
20
|
+
environment:
|
|
21
|
+
HTTPS_PROXY: http://egress:3128
|
|
22
|
+
HTTP_PROXY: http://egress:3128
|
|
23
|
+
NO_PROXY: 127.0.0.1,localhost
|
|
24
|
+
# Playwright downloads Chromium from a CDN; add it to the allowlist or
|
|
25
|
+
# pre-fetch it. Listed here so the failure is legible, not silent.
|
|
26
|
+
PLAYWRIGHT_DOWNLOAD_HOST: ${PLAYWRIGHT_DOWNLOAD_HOST:-}
|
|
27
|
+
|
|
28
|
+
egress:
|
|
29
|
+
image: ghcr.io/sinishadjukic/worca:${WORCA_TAG:-latest}
|
|
30
|
+
entrypoint: ["tini", "-s", "--", "node", "/usr/local/lib/worca-egress-proxy.mjs"]
|
|
31
|
+
command: []
|
|
32
|
+
environment:
|
|
33
|
+
WORCA_EGRESS_ALLOW: ${WORCA_EGRESS_ALLOW:-}
|
|
34
|
+
networks:
|
|
35
|
+
- internal
|
|
36
|
+
- default
|
|
37
|
+
security_opt:
|
|
38
|
+
- no-new-privileges:true
|
|
39
|
+
cap_drop:
|
|
40
|
+
- ALL
|
|
41
|
+
pids_limit: 128
|
|
42
|
+
mem_limit: 256m
|
|
43
|
+
restart: unless-stopped
|
|
44
|
+
|
|
45
|
+
networks:
|
|
46
|
+
internal:
|
|
47
|
+
internal: true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# docker/compose.ssh.yml — forward the host's SSH agent (docs/docker.md, "Git credentials").
|
|
2
|
+
#
|
|
3
|
+
# docker compose -f compose.yml -f compose.ssh.yml up -d
|
|
4
|
+
#
|
|
5
|
+
# Keys never enter the container; the agent socket does. While it is mounted an
|
|
6
|
+
# agent CAN use it, so pair this with the egress overlay for untrusted tasks.
|
|
7
|
+
#
|
|
8
|
+
# Docker Desktop (macOS/Windows): the socket is /run/host-services/ssh-auth.sock
|
|
9
|
+
# inside the VM — leave WORCA_SSH_SOCK unset.
|
|
10
|
+
# Linux Engine / Podman: WORCA_SSH_SOCK=$SSH_AUTH_SOCK in .env
|
|
11
|
+
|
|
12
|
+
services:
|
|
13
|
+
worca:
|
|
14
|
+
volumes:
|
|
15
|
+
- ${WORCA_SSH_SOCK:-/run/host-services/ssh-auth.sock}:/ssh-agent.sock
|
|
16
|
+
environment:
|
|
17
|
+
SSH_AUTH_SOCK: /ssh-agent.sock
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# docker/compose.teams.yml — Microsoft Teams webhook ingress (docs/docker.md, "Teams").
|
|
2
|
+
#
|
|
3
|
+
# docker compose -f compose.yml -f compose.teams.yml up -d
|
|
4
|
+
#
|
|
5
|
+
# Teams is the one chat channel that needs an inbound HTTPS URL (Bot Framework
|
|
6
|
+
# has no polling mode). A cloudflared sidecar dials OUT to Cloudflare and
|
|
7
|
+
# forwards to worca's token-guarded ingress route; nothing is published.
|
|
8
|
+
#
|
|
9
|
+
# CLOUDFLARE_TUNNEL_TOKEN a named tunnel's token (recommended; stable URL)
|
|
10
|
+
# unset: a Quick Tunnel with a random trycloudflare.com
|
|
11
|
+
# URL printed in `docker compose logs teams-tunnel`
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
teams-tunnel:
|
|
15
|
+
image: cloudflare/cloudflared:latest
|
|
16
|
+
command: >
|
|
17
|
+
tunnel --no-autoupdate
|
|
18
|
+
${CLOUDFLARE_TUNNEL_TOKEN:+run --token }${CLOUDFLARE_TUNNEL_TOKEN:---url http://worca:4317}
|
|
19
|
+
depends_on:
|
|
20
|
+
- worca
|
|
21
|
+
security_opt:
|
|
22
|
+
- no-new-privileges:true
|
|
23
|
+
cap_drop:
|
|
24
|
+
- ALL
|
|
25
|
+
restart: unless-stopped
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# docker/compose.yml — run Worca in a container (docs/docker.md).
|
|
2
|
+
#
|
|
3
|
+
# mkdir worca && cd worca
|
|
4
|
+
# curl -fsSLO https://raw.githubusercontent.com/SinishaDjukic/worca-cc/dev/docker/compose.yml
|
|
5
|
+
# echo "WORCA_PROJECTS=$HOME/dev" > .env
|
|
6
|
+
# docker compose up -d # http://localhost:4317
|
|
7
|
+
# docker compose run --rm worca claude # log Claude Code in, once
|
|
8
|
+
#
|
|
9
|
+
# Everything you change is a variable in .env (see docker/.env.example); this
|
|
10
|
+
# file is not edited. Overlays (egress allowlist, SSH agent, Teams tunnel,
|
|
11
|
+
# clone-in, dev) are separate files added with -f.
|
|
12
|
+
|
|
13
|
+
name: worca
|
|
14
|
+
|
|
15
|
+
services:
|
|
16
|
+
worca:
|
|
17
|
+
image: ghcr.io/sinishadjukic/worca:${WORCA_TAG:-latest}
|
|
18
|
+
init: true
|
|
19
|
+
user: "${WORCA_UID:-1000}:${WORCA_GID:-1000}"
|
|
20
|
+
ports:
|
|
21
|
+
# Loopback only. The server's Host/Origin guard accepts localhost, and
|
|
22
|
+
# nothing else should reach an unauthenticated UI that runs agents.
|
|
23
|
+
- "127.0.0.1:${WORCA_PORT:-4317}:4317"
|
|
24
|
+
volumes:
|
|
25
|
+
- worca-home:/worca # DB, store, runs, plugins, ui.json
|
|
26
|
+
- claude-config:/home/worca/.claude # Claude Code login, settings, memory
|
|
27
|
+
# Your repositories. Same path on both sides (path parity) so git
|
|
28
|
+
# worktree metadata stays valid on the host; see docs/docker.md.
|
|
29
|
+
- ${WORCA_PROJECTS:-${HOME}/dev}:${WORCA_PROJECTS_MOUNT:-${WORCA_PROJECTS:-${HOME}/dev}}
|
|
30
|
+
environment:
|
|
31
|
+
TZ: ${TZ:-UTC}
|
|
32
|
+
WORCA_PROJECTS_ROOT: ${WORCA_PROJECTS_MOUNT:-${WORCA_PROJECTS:-${HOME}/dev}}
|
|
33
|
+
GIT_AUTHOR_NAME: ${GIT_AUTHOR_NAME:-}
|
|
34
|
+
GIT_AUTHOR_EMAIL: ${GIT_AUTHOR_EMAIL:-}
|
|
35
|
+
GIT_COMMITTER_NAME: ${GIT_AUTHOR_NAME:-}
|
|
36
|
+
GIT_COMMITTER_EMAIL: ${GIT_AUTHOR_EMAIL:-}
|
|
37
|
+
GH_TOKEN: ${GH_TOKEN:-}
|
|
38
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN:-}
|
|
39
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
40
|
+
HTTPS_PROXY: ${HTTPS_PROXY:-}
|
|
41
|
+
HTTP_PROXY: ${HTTP_PROXY:-}
|
|
42
|
+
NO_PROXY: ${NO_PROXY:-127.0.0.1,localhost}
|
|
43
|
+
WORCA_MOCK: ${WORCA_MOCK:-}
|
|
44
|
+
security_opt:
|
|
45
|
+
- no-new-privileges:true
|
|
46
|
+
cap_drop:
|
|
47
|
+
- ALL
|
|
48
|
+
pids_limit: 2048
|
|
49
|
+
mem_limit: ${WORCA_MEM:-6g}
|
|
50
|
+
cpus: ${WORCA_CPUS:-4}
|
|
51
|
+
tmpfs:
|
|
52
|
+
- /tmp:size=2g
|
|
53
|
+
restart: unless-stopped
|
|
54
|
+
|
|
55
|
+
volumes:
|
|
56
|
+
worca-home:
|
|
57
|
+
claude-config:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-rc.2",
|
|
4
4
|
"description": "Worca — deterministic multi-agent pipeline that drives Claude Code (headless) through Plan -> Refine -> Implement -> Review, with a CLI, an installable /worca skill, and a web UI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"skills/",
|
|
39
39
|
"scripts/",
|
|
40
40
|
"tools/install.mjs",
|
|
41
|
+
"docker/compose*.yml",
|
|
42
|
+
"docker/.env.example",
|
|
41
43
|
"README.md"
|
|
42
44
|
],
|
|
43
45
|
"scripts": {
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"smoke:workspace": "WORCA_MOCK=1 WORCA_HOME=.worca-cc-smoke node --disable-warning=ExperimentalWarning tools/smoke-workspace.mjs",
|
|
50
52
|
"smoke:plugin": "WORCA_MOCK=1 WORCA_HOME=.worca-cc-smoke node --disable-warning=ExperimentalWarning tools/smoke-plugin.mjs",
|
|
51
53
|
"docker:build": "node tools/docker-build.mjs",
|
|
54
|
+
"docker:smoke": "node tools/docker-smoke.mjs",
|
|
52
55
|
"ask:fixtures": "node --disable-warning=ExperimentalWarning tools/ask-capture-fixtures.mjs",
|
|
53
56
|
"verify:composer": "node --disable-warning=ExperimentalWarning tools/verify-composer-cdp.mjs",
|
|
54
57
|
"verify:run-monitor": "node --disable-warning=ExperimentalWarning tools/verify-run-monitor-cdp.mjs",
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/cli/container.mjs
|
|
2
|
+
// `worca container` — run Worca in a container from the npm install
|
|
3
|
+
// (docs/docker.md; plans/container-isolation-design.md §12.3). A thin wrapper:
|
|
4
|
+
// it writes the package's compose files into <worcaHome>/container/, seeds a
|
|
5
|
+
// .env once, and shells out to `docker compose` (or `podman compose`). Nothing
|
|
6
|
+
// here talks to the engine; the box runs its own Worca with its own home.
|
|
7
|
+
//
|
|
8
|
+
// Every verb takes { out, c, fail } from the CLI and an injectable `exec` so
|
|
9
|
+
// tests never spawn a runtime.
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import process from 'node:process';
|
|
17
|
+
|
|
18
|
+
import { worcaHome, listProjects } from '../core/projects.mjs';
|
|
19
|
+
|
|
20
|
+
/** The compose files shipped in the package (package.json `files`). */
|
|
21
|
+
export const COMPOSE_SRC_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'docker');
|
|
22
|
+
export const OVERLAYS = Object.freeze(['egress', 'ssh', 'teams', 'clonein']);
|
|
23
|
+
const STATE_FILE = '.worca-container.json';
|
|
24
|
+
|
|
25
|
+
export const CONTAINER_HELP = `worca container — run Worca in a container (docs/docker.md)
|
|
26
|
+
|
|
27
|
+
worca container init [--projects <dir>] Write the compose files and a .env into the container dir
|
|
28
|
+
worca container up [--with <overlays>] [--tag <t>]
|
|
29
|
+
Start the box in the background and print the URL.
|
|
30
|
+
--with: comma-separated egress,ssh,teams,clonein (remembered)
|
|
31
|
+
worca container down Stop and remove the containers (volumes are kept)
|
|
32
|
+
worca container status Which containers are up
|
|
33
|
+
worca container logs [--follow] Server logs
|
|
34
|
+
worca container pull Pull the newest image for the tag in .env
|
|
35
|
+
worca container login Log Claude Code in, once, into the box's own volume
|
|
36
|
+
worca container shell A bash shell inside the box
|
|
37
|
+
worca container run -- <worca args> Run the Worca CLI inside the box
|
|
38
|
+
e.g. worca container run -- --project /path/to/repo --prompt "…"
|
|
39
|
+
worca container where Print the container dir (compose files, .env)
|
|
40
|
+
worca container help
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--dir <d> Container dir (default: <worcaHome>/container)
|
|
44
|
+
--runtime <r> docker | podman (default: autodetect; env WORCA_CONTAINER_RUNTIME)
|
|
45
|
+
|
|
46
|
+
The box has its own Worca home and its own Claude Code login: nothing from
|
|
47
|
+
~/.worca-cc or ~/.claude is mounted. Your repositories are mounted at the same
|
|
48
|
+
path as on the host (WORCA_PROJECTS in .env), so run worktrees stay valid here.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/** Default exec: run and inherit the terminal. Returns { status }. */
|
|
52
|
+
function defaultExec(cmd, args, { cwd, capture = false, env } = {}) {
|
|
53
|
+
const r = spawnSync(cmd, args, { cwd, env, stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', encoding: 'utf8' });
|
|
54
|
+
return { status: r.error ? 127 : (r.status ?? 1), stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `docker compose` or `podman compose`, by flag, env, then autodetect. */
|
|
58
|
+
export function detectRuntime(exec, want) {
|
|
59
|
+
const pick = want || process.env.WORCA_CONTAINER_RUNTIME || '';
|
|
60
|
+
const candidates = pick ? [pick] : ['docker', 'podman'];
|
|
61
|
+
for (const bin of candidates) {
|
|
62
|
+
if (!['docker', 'podman'].includes(bin)) return { error: `--runtime must be docker or podman, got ${bin}` };
|
|
63
|
+
if (exec(bin, ['compose', 'version'], { capture: true }).status === 0) return { bin };
|
|
64
|
+
}
|
|
65
|
+
return { error: pick ? `${pick} compose is not available` : 'no container runtime found: install Docker Desktop, Docker Engine or Podman (with podman compose)' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parse(argv) {
|
|
69
|
+
const o = { verb: argv[0] || 'help', dir: null, runtime: null, projects: null, with: null, tag: null, follow: false, passthrough: [], _: [] };
|
|
70
|
+
const rest = argv.slice(1);
|
|
71
|
+
for (let i = 0; i < rest.length; i++) {
|
|
72
|
+
const a = rest[i];
|
|
73
|
+
if (a === '--') { o.passthrough = rest.slice(i + 1); break; }
|
|
74
|
+
else if (a === '--dir') o.dir = rest[++i];
|
|
75
|
+
else if (a === '--runtime') o.runtime = rest[++i];
|
|
76
|
+
else if (a === '--projects') o.projects = rest[++i];
|
|
77
|
+
else if (a === '--with') o.with = rest[++i];
|
|
78
|
+
else if (a === '--tag') o.tag = rest[++i];
|
|
79
|
+
else if (a === '--follow' || a === '-f') o.follow = true;
|
|
80
|
+
else o._.push(a);
|
|
81
|
+
}
|
|
82
|
+
return o;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Longest common ancestor directory of the registered projects, or null. */
|
|
86
|
+
export function commonParent(paths) {
|
|
87
|
+
const parts = paths.filter(Boolean).map((p) => resolve(p).split(/[\\/]+/));
|
|
88
|
+
if (!parts.length) return null;
|
|
89
|
+
let prefix = parts[0].slice(0, -1); // a project's parent, never the project itself
|
|
90
|
+
for (const p of parts.slice(1)) {
|
|
91
|
+
let n = 0;
|
|
92
|
+
while (n < prefix.length && n < p.length - 1 && prefix[n] === p[n]) n++;
|
|
93
|
+
prefix = prefix.slice(0, n);
|
|
94
|
+
}
|
|
95
|
+
if (prefix.length <= 1) return null; // "/" or a drive root is too wide to mount
|
|
96
|
+
return prefix.join('/') || '/';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The .env body for a fresh container dir: the example with the known values filled in. */
|
|
100
|
+
export function seedEnv(example, { projects, tz, gitName, gitEmail }) {
|
|
101
|
+
const set = (body, key, value) => {
|
|
102
|
+
if (!value) return body;
|
|
103
|
+
const re = new RegExp(`^#?${key}=.*$`, 'm');
|
|
104
|
+
return re.test(body) ? body.replace(re, `${key}=${value}`) : `${body}\n${key}=${value}\n`;
|
|
105
|
+
};
|
|
106
|
+
let body = example;
|
|
107
|
+
body = set(body, 'WORCA_PROJECTS', projects);
|
|
108
|
+
body = set(body, 'TZ', tz);
|
|
109
|
+
body = set(body, 'GIT_AUTHOR_NAME', gitName);
|
|
110
|
+
body = set(body, 'GIT_AUTHOR_EMAIL', gitEmail);
|
|
111
|
+
return body;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readState(dir) {
|
|
115
|
+
try { return JSON.parse(readFileSync(join(dir, STATE_FILE), 'utf8')); } catch { return {}; }
|
|
116
|
+
}
|
|
117
|
+
function writeState(dir, state) {
|
|
118
|
+
writeFileSync(join(dir, STATE_FILE), JSON.stringify(state, null, 2) + '\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `-f compose.yml -f compose.<overlay>.yml …` for the remembered or requested overlays. */
|
|
122
|
+
export function composeFileArgs(overlays) {
|
|
123
|
+
const args = ['-f', 'compose.yml'];
|
|
124
|
+
for (const o of overlays) args.push('-f', `compose.${o}.yml`);
|
|
125
|
+
return args;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseWith(s) {
|
|
129
|
+
if (!s) return [];
|
|
130
|
+
const list = s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
131
|
+
const bad = list.filter((x) => !OVERLAYS.includes(x));
|
|
132
|
+
if (bad.length) return { error: `--with: unknown overlay ${bad.join(', ')} (known: ${OVERLAYS.join(', ')})` };
|
|
133
|
+
return [...new Set(list)];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Copy the package's compose files into `dir` (always, they are versioned with the package)
|
|
137
|
+
* and seed .env once. Returns { dir, envCreated }. */
|
|
138
|
+
export async function initDir(dir, { projects, exec, srcDir = COMPOSE_SRC_DIR, env = process.env } = {}) {
|
|
139
|
+
mkdirSync(dir, { recursive: true });
|
|
140
|
+
for (const f of readdirSync(srcDir)) {
|
|
141
|
+
if (/^compose(\.[a-z]+)?\.yml$/.test(f)) copyFileSync(join(srcDir, f), join(dir, f));
|
|
142
|
+
}
|
|
143
|
+
const envPath = join(dir, '.env');
|
|
144
|
+
let envCreated = false;
|
|
145
|
+
if (!existsSync(envPath)) {
|
|
146
|
+
let root = projects;
|
|
147
|
+
if (!root) {
|
|
148
|
+
try { root = commonParent((await listProjects()).map((p) => p.path)); } catch { root = null; }
|
|
149
|
+
}
|
|
150
|
+
if (!root) root = join(env.HOME || env.USERPROFILE || homedir(), 'dev');
|
|
151
|
+
const git = (k) => { const r = exec('git', ['config', '--get', k], { capture: true }); return r.status === 0 ? r.stdout.trim() : ''; };
|
|
152
|
+
const body = seedEnv(readFileSync(join(srcDir, '.env.example'), 'utf8'), {
|
|
153
|
+
projects: root,
|
|
154
|
+
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
155
|
+
gitName: git('user.name'),
|
|
156
|
+
gitEmail: git('user.email'),
|
|
157
|
+
});
|
|
158
|
+
writeFileSync(envPath, body, { mode: 0o600 });
|
|
159
|
+
envCreated = true;
|
|
160
|
+
}
|
|
161
|
+
return { dir, envCreated };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readEnvValue(dir, key) {
|
|
165
|
+
try {
|
|
166
|
+
const m = new RegExp(`^${key}=(.*)$`, 'm').exec(readFileSync(join(dir, '.env'), 'utf8'));
|
|
167
|
+
return m ? m[1].trim() : '';
|
|
168
|
+
} catch { return ''; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `worca container <verb> …`. Returns the process exit code.
|
|
173
|
+
* @param {string[]} argv tokens after `container`
|
|
174
|
+
* @param {object} io { out, c, fail } from the CLI; `exec` and `dir` are injectable for tests
|
|
175
|
+
*/
|
|
176
|
+
export async function cmdContainer(argv, { out, c, fail, exec = defaultExec, dir: dirOverride = null } = {}) {
|
|
177
|
+
const a = parse(argv);
|
|
178
|
+
if (a.verb === 'help' || a.verb === '--help' || a.verb === '-h') { out(CONTAINER_HELP.trimEnd()); return 0; }
|
|
179
|
+
const dir = resolve(a.dir || dirOverride || join(worcaHome(), 'container'));
|
|
180
|
+
if (a.verb === 'where') { out(dir); return 0; }
|
|
181
|
+
|
|
182
|
+
const rt = detectRuntime(exec, a.runtime);
|
|
183
|
+
if (rt.error) { fail(rt.error); return 1; }
|
|
184
|
+
const state = readState(dir);
|
|
185
|
+
let overlays = Array.isArray(state.with) ? state.with : [];
|
|
186
|
+
const compose = (args, opts = {}) => exec(rt.bin, ['compose', ...composeFileArgs(overlays), ...args], { cwd: dir, ...opts }).status;
|
|
187
|
+
|
|
188
|
+
if (a.verb === 'init' || a.verb === 'up') {
|
|
189
|
+
const r = await initDir(dir, { projects: a.projects, exec });
|
|
190
|
+
out(`${c('bold', 'container dir')} ${dir}${r.envCreated ? ` (${c('green', '.env created')} — edit it: WORCA_PROJECTS, tokens)` : ''}`);
|
|
191
|
+
if (a.verb === 'init') return 0;
|
|
192
|
+
}
|
|
193
|
+
if (!existsSync(join(dir, 'compose.yml'))) { fail(`no compose files in ${dir}; run: worca container init`); return 1; }
|
|
194
|
+
|
|
195
|
+
switch (a.verb) {
|
|
196
|
+
case 'up': {
|
|
197
|
+
if (a.with !== null) {
|
|
198
|
+
const parsed = parseWith(a.with);
|
|
199
|
+
if (parsed.error) { fail(parsed.error); return 1; }
|
|
200
|
+
overlays = parsed;
|
|
201
|
+
writeState(dir, { ...state, with: overlays });
|
|
202
|
+
}
|
|
203
|
+
const env = a.tag ? { ...process.env, WORCA_TAG: a.tag } : undefined;
|
|
204
|
+
const port = readEnvValue(dir, 'WORCA_PORT') || '4317';
|
|
205
|
+
const code = exec(rt.bin, ['compose', ...composeFileArgs(overlays), 'up', '-d'], { cwd: dir, env }).status;
|
|
206
|
+
if (code !== 0) return code;
|
|
207
|
+
out(`${c('bold', 'Worca')} http://localhost:${port}${overlays.length ? ` overlays: ${overlays.join(', ')}` : ''}`);
|
|
208
|
+
out(` Log in once: ${c('bold', 'worca container login')}`);
|
|
209
|
+
out(` Stop: ${c('bold', 'worca container down')}`);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
case 'down': return compose(['down']);
|
|
213
|
+
case 'status': return compose(['ps']);
|
|
214
|
+
case 'logs': return compose(['logs', ...(a.follow ? ['-f'] : []), 'worca']);
|
|
215
|
+
case 'pull': return compose(['pull', 'worca']);
|
|
216
|
+
case 'login': return compose(['run', '--rm', 'worca', 'claude']);
|
|
217
|
+
case 'shell': return compose(['run', '--rm', 'worca', 'bash']);
|
|
218
|
+
case 'run': {
|
|
219
|
+
const args = a.passthrough.length ? a.passthrough : a._;
|
|
220
|
+
if (!args.length) { fail('Usage: worca container run -- <worca args>'); return 1; }
|
|
221
|
+
return compose(['run', '--rm', 'worca', 'worca', ...args]);
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
fail(`unknown container verb: ${a.verb}\n\n${CONTAINER_HELP}`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/cli/models.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
endpointModelsForImport, importEndpointModels,
|
|
14
14
|
} from '../core/bridge/provider-ops.mjs';
|
|
15
15
|
import { listModels } from '../core/config.mjs';
|
|
16
|
+
import { isTranslatedApi } from '../core/model-env.mjs';
|
|
16
17
|
|
|
17
18
|
export const MODELS_HELP = `worca models — the model catalog and its providers
|
|
18
19
|
|
|
@@ -76,7 +77,7 @@ export function formatModelLine(m) {
|
|
|
76
77
|
else if (m.custom === 'policy') bits.push('policy');
|
|
77
78
|
else if (m.custom === 'global') bits.push('yours');
|
|
78
79
|
else if (!m.custom) bits.push('built-in');
|
|
79
|
-
if (m.bridged) bits.push(`bridged: ${m.bridged}${m.upstreamModel ? ` → ${m.upstreamModel}` : ''}${m.upstreamApi
|
|
80
|
+
if (m.bridged) bits.push(`bridged: ${m.bridged}${m.upstreamModel ? ` → ${m.upstreamModel}` : ''}${isTranslatedApi(m.upstreamApi) ? ' (translated)' : ''}`);
|
|
80
81
|
else if (m.routed) bits.push('endpoint-routed');
|
|
81
82
|
if (m.needsSignIn) bits.push(`NEEDS ${m.signInReason === 'no_key' ? 'API KEY' : m.signInReason === 'terms' ? 'ACKNOWLEDGEMENT' : 'SIGN-IN'}`);
|
|
82
83
|
if (m.hidden) bits.push('hidden');
|
|
@@ -208,14 +209,14 @@ export async function cmdModels(argv, { out, c, fail, sleep: wait = sleep }) {
|
|
|
208
209
|
else if (args.all) ids = importable.map((m) => m.id);
|
|
209
210
|
else {
|
|
210
211
|
out('Copilot models (pass --all, or --pick id,id,…):');
|
|
211
|
-
for (const m of list) out(` ${m.id.padEnd(28)} ${m.vendor.padEnd(10)} ${(m.contextWindow ? `${Math.round(m.contextWindow / 1000)}k` : '-').padStart(5)} ${m.toolCalls ? 'tools' : ' '} ${m.vision ? 'vision' : ' '} ${m.reasoning ? 'reasoning' : ' '}${m.inCatalog ? ' (in catalog)' : ''}${m.policyState && m.policyState !== 'enabled' ? ' (disabled in Copilot settings)' : ''}`);
|
|
212
|
+
for (const m of list) out(` ${m.id.padEnd(28)} ${m.vendor.padEnd(10)} ${(m.contextWindow ? `${Math.round(m.contextWindow / 1000)}k` : '-').padStart(5)} ${m.toolCalls ? 'tools' : ' '} ${m.vision ? 'vision' : ' '} ${m.reasoning ? 'reasoning' : ' '} ${String(m.api || '').padEnd(16)}${m.inCatalog ? ' (in catalog)' : ''}${m.policyState && m.policyState !== 'enabled' ? ' (disabled in Copilot settings)' : ''}`);
|
|
212
213
|
return 0;
|
|
213
214
|
}
|
|
214
|
-
out('These run through your Copilot subscription. Claude models keep extended thinking; other vendors run through a translation layer (no
|
|
215
|
+
out('These run through your Copilot subscription. Claude models keep extended thinking; other vendors run through a translation layer (no web tools; reasoning arrives as summaries on the Responses API).');
|
|
215
216
|
if (!(await confirm(`Import ${ids.length} model${ids.length === 1 ? '' : 's'} into the catalog?`, args.yes, { c }))) return fail('cancelled');
|
|
216
217
|
const r = await importCopilotModels(ids);
|
|
217
218
|
for (const id of r.created) out(` + ${id}`);
|
|
218
|
-
for (const id of r.updated) out(` ~ ${id} (capabilities refreshed)`);
|
|
219
|
+
for (const id of r.updated) out(` ~ ${id} (API and capabilities refreshed)`);
|
|
219
220
|
for (const id of r.skipped) out(` - ${id} (skipped: not offered, or not a copilot entry)`);
|
|
220
221
|
return 0;
|
|
221
222
|
}
|