@bankung/agent-teams 0.1.0
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/.env.example +289 -0
- package/README.md +143 -0
- package/cli/README.md +140 -0
- package/cli/index.js +665 -0
- package/cli/lib/confirm.js +30 -0
- package/cli/lib/docker.js +93 -0
- package/cli/lib/env.js +101 -0
- package/cli/lib/health.js +53 -0
- package/cli/lib/open-url.js +60 -0
- package/docker-compose.images.yml +177 -0
- package/package.json +33 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// cli/lib/docker.js — Docker daemon check + compose runner helpers.
|
|
3
|
+
// Zero runtime dependencies; uses only Node built-ins.
|
|
4
|
+
|
|
5
|
+
const { spawnSync, spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check whether the `git` CLI is on PATH.
|
|
9
|
+
* Returns { ok: true } or { ok: false, message: string }.
|
|
10
|
+
*/
|
|
11
|
+
function checkGit() {
|
|
12
|
+
const result = spawnSync('git', ['--version'], { stdio: 'pipe', shell: false });
|
|
13
|
+
if (result.error || result.status === null) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
message: [
|
|
17
|
+
'git is not installed or not on PATH.',
|
|
18
|
+
'Install Git (https://git-scm.com/downloads) and re-run this command.',
|
|
19
|
+
'git is required to clone the agent-teams repository in standalone mode.',
|
|
20
|
+
].join('\n'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check whether the Docker CLI is on PATH and the daemon is reachable.
|
|
28
|
+
* Returns { ok: true } or { ok: false, message: string }.
|
|
29
|
+
*/
|
|
30
|
+
function checkDocker() {
|
|
31
|
+
// 1. Is `docker` on PATH?
|
|
32
|
+
const whichResult = spawnSync('docker', ['--version'], { stdio: 'pipe', shell: false });
|
|
33
|
+
if (whichResult.error || whichResult.status === null) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
message: [
|
|
37
|
+
'Docker is not installed or not on PATH.',
|
|
38
|
+
'Install Docker Desktop (https://docs.docker.com/get-docker/) and re-run this command.',
|
|
39
|
+
'Docker is a required prerequisite — npm does not install it.',
|
|
40
|
+
].join('\n'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. Is the daemon responding? (`docker info` exits non-zero when daemon is down.)
|
|
45
|
+
// F-04: timeout:10000 prevents a hung daemon from freezing the install indefinitely.
|
|
46
|
+
const infoResult = spawnSync('docker', ['info'], { stdio: 'pipe', shell: false, timeout: 10000 });
|
|
47
|
+
if (infoResult.status !== 0) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: [
|
|
51
|
+
'Docker is installed but the daemon is not responding.',
|
|
52
|
+
'Start Docker Desktop (or run `sudo systemctl start docker` on Linux) and re-run:',
|
|
53
|
+
' npx agent-teams up',
|
|
54
|
+
'Troubleshooting: https://docs.docker.com/get-docker/',
|
|
55
|
+
].join('\n'),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run `docker compose -p agent-teams [-f <composeFile>] <args>` with stdio inherited.
|
|
64
|
+
* Returns exit code (integer). Streams all output directly to the terminal.
|
|
65
|
+
*
|
|
66
|
+
* @param {string[]} args Arguments after the compose preamble
|
|
67
|
+
* @param {object} [env] Additional env vars to merge (e.g. { MIGRATION_TARGET: 'live' })
|
|
68
|
+
* @param {object} [spawnOpts] Extra options forwarded to spawn() (e.g. { cwd: repoRoot })
|
|
69
|
+
* @param {string} [composeFile] Optional path to an alternate compose file (e.g. docker-compose.images.yml).
|
|
70
|
+
* When omitted, Docker uses the default docker-compose.yml discovery.
|
|
71
|
+
*/
|
|
72
|
+
function compose(args, env = {}, spawnOpts = {}, composeFile = null) {
|
|
73
|
+
const fileArgs = composeFile ? ['-f', composeFile] : [];
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const child = spawn(
|
|
76
|
+
'docker',
|
|
77
|
+
['compose', '-p', 'agent-teams', ...fileArgs, ...args],
|
|
78
|
+
{
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
shell: false,
|
|
81
|
+
env: { ...process.env, ...env },
|
|
82
|
+
...spawnOpts,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
86
|
+
child.on('error', (err) => {
|
|
87
|
+
process.stderr.write(`ERROR: failed to spawn docker: ${err.message}\n`);
|
|
88
|
+
resolve(1);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { checkGit, checkDocker, compose };
|
package/cli/lib/env.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// cli/lib/env.js — .env scaffold + CREDENTIALS_MASTER_KEY Fernet generation.
|
|
3
|
+
// Zero runtime dependencies; uses only Node built-ins.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure .env exists (copied from .env.example if absent) and that
|
|
11
|
+
* CREDENTIALS_MASTER_KEY is set to a valid Fernet key.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent: never overwrites an existing non-empty key.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} repoRoot Absolute path to the repo root.
|
|
16
|
+
*/
|
|
17
|
+
function ensureEnv(repoRoot) {
|
|
18
|
+
const envFile = path.join(repoRoot, '.env');
|
|
19
|
+
const envExample = path.join(repoRoot, '.env.example');
|
|
20
|
+
|
|
21
|
+
// ---- scaffold .env from .env.example if missing --------------------------
|
|
22
|
+
if (!fs.existsSync(envFile)) {
|
|
23
|
+
if (fs.existsSync(envExample)) {
|
|
24
|
+
fs.copyFileSync(envExample, envFile);
|
|
25
|
+
// chmod 0600: no-op on Windows but protects .env on shared POSIX hosts.
|
|
26
|
+
try { fs.chmodSync(envFile, 0o600); } catch (_) { /* Windows — ignore */ }
|
|
27
|
+
console.log('==> .env not found — copied from .env.example.');
|
|
28
|
+
} else {
|
|
29
|
+
console.warn('WARN: .env.example not found. You may need to create .env manually.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---- CREDENTIALS_MASTER_KEY ----------------------------------------------
|
|
34
|
+
if (!fs.existsSync(envFile)) return; // nothing to do if we still have no .env
|
|
35
|
+
|
|
36
|
+
let content = fs.readFileSync(envFile, 'utf8');
|
|
37
|
+
|
|
38
|
+
// Match the line only when the value is non-empty (non-whitespace after '=').
|
|
39
|
+
const keyPresent = /^CREDENTIALS_MASTER_KEY=\S+/m.test(content);
|
|
40
|
+
|
|
41
|
+
if (keyPresent) {
|
|
42
|
+
console.log('==> CREDENTIALS_MASTER_KEY already set — leaving untouched.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Generate: URL-safe base64 of 32 random bytes.
|
|
47
|
+
// Node's 'base64url' encoding (available since Node 15) produces the exact
|
|
48
|
+
// URL-safe alphabet (- and _ instead of + and /) that Python's Fernet expects.
|
|
49
|
+
// 32 bytes → 44-char base64url string (no padding added by 'base64url').
|
|
50
|
+
// Fernet requires standard base64 padding; we append '=' to make it 44 chars.
|
|
51
|
+
//
|
|
52
|
+
// Actually: 32 bytes → ceil(32/3)*4 = 44 chars in standard base64.
|
|
53
|
+
// base64url omits the trailing '=' but Fernet checks for it.
|
|
54
|
+
// We generate via 'base64' then swap +/ → -_ to stay explicit about format.
|
|
55
|
+
const rawB64 = crypto.randomBytes(32).toString('base64'); // 44 chars, ends with '='
|
|
56
|
+
const fernetKey = rawB64.replace(/\+/g, '-').replace(/\//g, '_');
|
|
57
|
+
|
|
58
|
+
console.log('==> CREDENTIALS_MASTER_KEY is missing/empty — generating a Fernet key...');
|
|
59
|
+
|
|
60
|
+
// Replace an existing empty-value line, or append if the line is absent.
|
|
61
|
+
if (/^CREDENTIALS_MASTER_KEY=/m.test(content)) {
|
|
62
|
+
content = content.replace(/^CREDENTIALS_MASTER_KEY=.*/m, `CREDENTIALS_MASTER_KEY=${fernetKey}`);
|
|
63
|
+
} else {
|
|
64
|
+
content = content.trimEnd() + `\nCREDENTIALS_MASTER_KEY=${fernetKey}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(envFile, content, { encoding: 'utf8', mode: 0o600 });
|
|
68
|
+
// chmod 0600: defensive belt-and-suspenders in case umask overrode mode above.
|
|
69
|
+
try { fs.chmodSync(envFile, 0o600); } catch (_) { /* Windows — ignore */ }
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('NOTICE: A new CREDENTIALS_MASTER_KEY has been generated and written to .env.');
|
|
73
|
+
console.log(' Back it up securely (password manager / offline storage). Losing this');
|
|
74
|
+
console.log(' key makes ALL stored vault credentials permanently unrecoverable.');
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read a port from the .env file (if present) with a fallback default.
|
|
80
|
+
* Used so the health-wait targets the right port when API_PORT / WEB_PORT differ.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} repoRoot
|
|
83
|
+
* @param {string} varName e.g. 'API_PORT'
|
|
84
|
+
* @param {string} fallback e.g. '8456'
|
|
85
|
+
*/
|
|
86
|
+
function readEnvPort(repoRoot, varName, fallback) {
|
|
87
|
+
const envFile = path.join(repoRoot, '.env');
|
|
88
|
+
if (!fs.existsSync(envFile)) return fallback;
|
|
89
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
90
|
+
// Find the line for varName, then strip inline comments before matching digits.
|
|
91
|
+
// e.g. `API_PORT=8456 # note` — split at first '#', keep left side, then match.
|
|
92
|
+
// Escape varName to prevent regex injection (caller-controlled string).
|
|
93
|
+
const safeVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
const lineMatch = content.match(new RegExp(`^${safeVarName}=(.*)`, 'm'));
|
|
95
|
+
if (!lineMatch) return fallback;
|
|
96
|
+
const valueRaw = lineMatch[1].split('#')[0].trim(); // strip inline comment
|
|
97
|
+
const digitMatch = valueRaw.match(/^(\d+)/);
|
|
98
|
+
return digitMatch ? digitMatch[1] : fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { ensureEnv, readEnvPort };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// cli/lib/health.js — Poll an HTTP endpoint until it returns 200.
|
|
3
|
+
// Zero runtime dependencies; uses Node built-in `http` module.
|
|
4
|
+
|
|
5
|
+
const http = require('http');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Poll `url` every `intervalMs` until it returns HTTP 200 or `timeoutMs` elapses.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} url
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {number} [opts.timeoutMs=60000] Total wait budget in ms.
|
|
13
|
+
* @param {number} [opts.intervalMs=5000] Polling interval in ms.
|
|
14
|
+
* @param {number} [opts.probeTimeoutMs=5000] Per-request timeout in ms.
|
|
15
|
+
* @returns {Promise<boolean>} true if healthy, false if timed out.
|
|
16
|
+
*/
|
|
17
|
+
function waitForHealthy(url, { timeoutMs = 60000, intervalMs = 5000, probeTimeoutMs = 5000 } = {}) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const deadline = Date.now() + timeoutMs;
|
|
20
|
+
// F-03: track real wall-clock start so elapsed reflects actual seconds waited.
|
|
21
|
+
const startedAt = Date.now();
|
|
22
|
+
|
|
23
|
+
function probe() {
|
|
24
|
+
const req = http.get(url, { timeout: probeTimeoutMs }, (res) => {
|
|
25
|
+
res.resume(); // drain response body
|
|
26
|
+
if (res.statusCode === 200) {
|
|
27
|
+
resolve(true);
|
|
28
|
+
} else {
|
|
29
|
+
scheduleNext();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
req.on('error', () => scheduleNext());
|
|
33
|
+
req.on('timeout', () => { req.destroy(); scheduleNext(); });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scheduleNext() {
|
|
37
|
+
// Use the full budget: only give up AFTER Date.now() has reached the deadline.
|
|
38
|
+
// The old check (Date.now() + intervalMs > deadline) abandoned the last probe
|
|
39
|
+
// ~1 interval early (wastes ~5s of a 60s cap).
|
|
40
|
+
if (Date.now() >= deadline) {
|
|
41
|
+
resolve(false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
45
|
+
process.stdout.write(` ...still waiting (${elapsed}s elapsed)\n`);
|
|
46
|
+
setTimeout(probe, intervalMs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
probe();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { waitForHealthy };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// cli/lib/open-url.js — Best-effort cross-platform browser opener.
|
|
3
|
+
// Always prints the URL; never throws or fails the caller.
|
|
4
|
+
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
// Safe URL pattern: only localhost URLs with a numeric port and a restricted path.
|
|
8
|
+
// This guards against metacharacter injection into cmd.exe on Windows even when
|
|
9
|
+
// shell:false is used (cmd /c start re-parses the URL argument).
|
|
10
|
+
// The port is already digit-constrained by readEnvPort, so the risk is low —
|
|
11
|
+
// this is defense-in-depth.
|
|
12
|
+
const SAFE_URL_RE = /^https?:\/\/localhost:\d+\/[A-Za-z0-9/_-]*$/;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Print `url` and attempt to open it in the default browser.
|
|
16
|
+
* Failure is silently swallowed — this must never block the install.
|
|
17
|
+
* If the URL does not match the safe pattern, the auto-open is skipped
|
|
18
|
+
* and only the printed URL is shown.
|
|
19
|
+
*
|
|
20
|
+
* SECURITY: This function is only safe for URLs constructed internally by the
|
|
21
|
+
* CLI (localhost + digit port + restricted path). Never pass user-supplied or
|
|
22
|
+
* externally-sourced URLs — the SAFE_URL_RE guard is defense-in-depth, not a
|
|
23
|
+
* general sanitiser.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} url Must be a compiler-controlled localhost URL (see SAFE_URL_RE).
|
|
26
|
+
*/
|
|
27
|
+
function openUrl(url) {
|
|
28
|
+
console.log(`\nOpen in your browser: ${url}\n`);
|
|
29
|
+
|
|
30
|
+
if (!SAFE_URL_RE.test(url)) {
|
|
31
|
+
// URL contains unexpected characters — skip auto-open, just rely on the print above.
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let cmd, args;
|
|
36
|
+
switch (process.platform) {
|
|
37
|
+
case 'win32':
|
|
38
|
+
// `start` is a cmd.exe builtin; run via cmd /c start
|
|
39
|
+
cmd = 'cmd';
|
|
40
|
+
args = ['/c', 'start', '', url];
|
|
41
|
+
break;
|
|
42
|
+
case 'darwin':
|
|
43
|
+
cmd = 'open';
|
|
44
|
+
args = [url];
|
|
45
|
+
break;
|
|
46
|
+
default: // Linux + everything else
|
|
47
|
+
cmd = 'xdg-open';
|
|
48
|
+
args = [url];
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, shell: false });
|
|
54
|
+
child.unref(); // don't keep the Node process alive waiting for the browser
|
|
55
|
+
} catch (_) {
|
|
56
|
+
// best-effort — ignore all errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { openUrl };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# docker-compose.images.yml — pull-based prod stack for agent-teams.
|
|
2
|
+
#
|
|
3
|
+
# Used by `npx @bankung/agent-teams up --images` to pull pre-built images from
|
|
4
|
+
# GHCR instead of building from source. No git clone required — the CLI ships
|
|
5
|
+
# this file and .env.example in the npm package.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# docker compose -p agent-teams -f docker-compose.images.yml pull
|
|
9
|
+
# docker compose -p agent-teams -f docker-compose.images.yml up -d
|
|
10
|
+
#
|
|
11
|
+
# Images are published by the GitHub Actions workflow
|
|
12
|
+
# .github/workflows/release-images.yml on every `v*` tag push.
|
|
13
|
+
# Set AGENT_TEAMS_VERSION to pin a specific release (default: latest).
|
|
14
|
+
#
|
|
15
|
+
# SYNC NOTICE: Keep env blocks, ports, healthchecks, and depends_on in sync
|
|
16
|
+
# with docker-compose.yml. The two files share the same env contract — any
|
|
17
|
+
# new env var added to docker-compose.yml must also be added here.
|
|
18
|
+
|
|
19
|
+
services:
|
|
20
|
+
db:
|
|
21
|
+
image: postgres:16-alpine
|
|
22
|
+
container_name: agent-teams-db
|
|
23
|
+
environment:
|
|
24
|
+
POSTGRES_USER: postgres
|
|
25
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
|
26
|
+
POSTGRES_DB: agent_teams
|
|
27
|
+
ports:
|
|
28
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
29
|
+
volumes:
|
|
30
|
+
- agent-teams-pgdata:/var/lib/postgresql/data
|
|
31
|
+
healthcheck:
|
|
32
|
+
test: ["CMD-SHELL", "pg_isready -U postgres -d agent_teams"]
|
|
33
|
+
interval: 5s
|
|
34
|
+
timeout: 5s
|
|
35
|
+
retries: 5
|
|
36
|
+
|
|
37
|
+
api:
|
|
38
|
+
image: ghcr.io/bankung/agent-teams-api:${AGENT_TEAMS_VERSION:-latest}
|
|
39
|
+
container_name: agent-teams-api
|
|
40
|
+
depends_on:
|
|
41
|
+
db:
|
|
42
|
+
condition: service_healthy
|
|
43
|
+
environment:
|
|
44
|
+
DATABASE_URL: postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/agent_teams
|
|
45
|
+
APP_ENV: ${APP_ENV:-production}
|
|
46
|
+
APP_DEBUG: ${APP_DEBUG:-false}
|
|
47
|
+
REPO_ROOT: /repo
|
|
48
|
+
LANGGRAPH_LLM_PROVIDER: ${LANGGRAPH_LLM_PROVIDER:-anthropic}
|
|
49
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
50
|
+
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
51
|
+
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-sonnet-4-6}
|
|
52
|
+
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o}
|
|
53
|
+
BACKUP_S3_BUCKET: ${BACKUP_S3_BUCKET:-}
|
|
54
|
+
BACKUP_S3_ACCESS_KEY_ID: ${BACKUP_S3_ACCESS_KEY_ID:-}
|
|
55
|
+
BACKUP_S3_SECRET_ACCESS_KEY: ${BACKUP_S3_SECRET_ACCESS_KEY:-}
|
|
56
|
+
BACKUP_AGE_PUBKEY: ${BACKUP_AGE_PUBKEY:-}
|
|
57
|
+
BACKUP_S3_REGION: ${BACKUP_S3_REGION:-us-east-1}
|
|
58
|
+
BACKUP_S3_ENDPOINT: ${BACKUP_S3_ENDPOINT:-}
|
|
59
|
+
BACKUP_S3_PREFIX: ${BACKUP_S3_PREFIX:-agent-teams/}
|
|
60
|
+
SECRET_KEY: ${SECRET_KEY:-}
|
|
61
|
+
GMAIL_SMTP_HOST: ${GMAIL_SMTP_HOST:-smtp.gmail.com}
|
|
62
|
+
GMAIL_SMTP_PORT: ${GMAIL_SMTP_PORT:-587}
|
|
63
|
+
GMAIL_SMTP_USER: ${GMAIL_SMTP_USER:-}
|
|
64
|
+
GMAIL_SMTP_APP_PASSWORD: ${GMAIL_SMTP_APP_PASSWORD:-}
|
|
65
|
+
GMAIL_SMTP_FROM: ${GMAIL_SMTP_FROM:-}
|
|
66
|
+
DIGEST_EMAIL_RECIPIENT: ${DIGEST_EMAIL_RECIPIENT:-}
|
|
67
|
+
DIGEST_EMAIL_ENABLED: ${DIGEST_EMAIL_ENABLED:-false}
|
|
68
|
+
WEB_BASE_URL: ${WEB_BASE_URL:-http://localhost:5431}
|
|
69
|
+
NTFY_BASE_URL: ${NTFY_BASE_URL:-https://ntfy.sh}
|
|
70
|
+
NTFY_TOPIC: ${NTFY_TOPIC:-}
|
|
71
|
+
NTFY_ACCESS_TOKEN: ${NTFY_ACCESS_TOKEN:-}
|
|
72
|
+
PUSH_ENABLED: ${PUSH_ENABLED:-false}
|
|
73
|
+
BACKUP_CRON_RULE: ${BACKUP_CRON_RULE:-0 3 * * *}
|
|
74
|
+
BACKUP_TIMEZONE: ${BACKUP_TIMEZONE:-UTC}
|
|
75
|
+
BACKUP_KEEP_DAILY: ${BACKUP_KEEP_DAILY:-30}
|
|
76
|
+
BACKUP_KEEP_MONTHLY: ${BACKUP_KEEP_MONTHLY:-12}
|
|
77
|
+
BACKUP_DRY_RUN: ${BACKUP_DRY_RUN:-false}
|
|
78
|
+
HEALTH_MONITOR_DISABLED: ${HEALTH_MONITOR_DISABLED:-}
|
|
79
|
+
HEALTH_MONITOR_INTERVAL_MINUTES: ${HEALTH_MONITOR_INTERVAL_MINUTES:-15}
|
|
80
|
+
HEALTH_MONITOR_API_BASE: ${HEALTH_MONITOR_API_BASE:-http://localhost:8456}
|
|
81
|
+
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
|
|
82
|
+
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
|
|
83
|
+
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
|
|
84
|
+
PYTEST_DB_PASSWORD: ${PYTEST_DB_PASSWORD:-}
|
|
85
|
+
CREDENTIALS_MASTER_KEY: ${CREDENTIALS_MASTER_KEY:-}
|
|
86
|
+
OPERATOR_ACTION_KEY: ${OPERATOR_ACTION_KEY:-}
|
|
87
|
+
GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-}
|
|
88
|
+
GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-}
|
|
89
|
+
GOOGLE_OAUTH_REDIRECT_URI: ${GOOGLE_OAUTH_REDIRECT_URI:-http://localhost:8456/api/tools/email/auth/gmail/callback}
|
|
90
|
+
EMAIL_TOOLS_DAILY_UNITS_CAP: ${EMAIL_TOOLS_DAILY_UNITS_CAP:-5000}
|
|
91
|
+
EMAIL_TOOLS_BULK_THRESHOLD: ${EMAIL_TOOLS_BULK_THRESHOLD:-100}
|
|
92
|
+
EMAIL_TOOLS_AUDIT_PATH: ${EMAIL_TOOLS_AUDIT_PATH:-/repo/_scratch/email-tools-audit.jsonl}
|
|
93
|
+
AZURE_OAUTH_CLIENT_ID: ${AZURE_OAUTH_CLIENT_ID:-}
|
|
94
|
+
AZURE_OAUTH_CLIENT_SECRET: ${AZURE_OAUTH_CLIENT_SECRET:-}
|
|
95
|
+
AZURE_OAUTH_TENANT: ${AZURE_OAUTH_TENANT:-consumers}
|
|
96
|
+
AZURE_OAUTH_REDIRECT_URI: ${AZURE_OAUTH_REDIRECT_URI:-http://localhost:8456/api/tools/email/auth/outlook/callback}
|
|
97
|
+
ports:
|
|
98
|
+
- "${API_PORT:-8456}:8456"
|
|
99
|
+
volumes:
|
|
100
|
+
# Named volume gives /repo a writable, persisted filesystem so scaffold/
|
|
101
|
+
# session/resource/audit writes (REPO_ROOT=/repo) succeed without a source
|
|
102
|
+
# bind-mount. Data persists across restarts; no repo source is exposed.
|
|
103
|
+
- agent-teams-repo-data:/repo
|
|
104
|
+
command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8456"]
|
|
105
|
+
healthcheck:
|
|
106
|
+
test: ["CMD-SHELL", "curl -fsS http://localhost:8456/health || exit 1"]
|
|
107
|
+
interval: 5s
|
|
108
|
+
timeout: 3s
|
|
109
|
+
retries: 5
|
|
110
|
+
start_period: 10s
|
|
111
|
+
|
|
112
|
+
web:
|
|
113
|
+
image: ghcr.io/bankung/agent-teams-web:${AGENT_TEAMS_VERSION:-latest}
|
|
114
|
+
container_name: agent-teams-web
|
|
115
|
+
depends_on:
|
|
116
|
+
api:
|
|
117
|
+
condition: service_healthy
|
|
118
|
+
environment:
|
|
119
|
+
# NEXT_PUBLIC_* vars are baked at image build time (next build ARGs), not
|
|
120
|
+
# injected at container runtime — setting them here has no effect on the
|
|
121
|
+
# client bundle. Only runtime-effective server-side vars belong here.
|
|
122
|
+
INTERNAL_API_URL: ${INTERNAL_API_URL:-http://api:8456}
|
|
123
|
+
ports:
|
|
124
|
+
- "${WEB_PORT:-5431}:5431"
|
|
125
|
+
# No source bind-mount — standalone bundle is baked into the image.
|
|
126
|
+
healthcheck:
|
|
127
|
+
test: ["CMD-SHELL", "wget -q --spider http://localhost:5431 || exit 1"]
|
|
128
|
+
interval: 10s
|
|
129
|
+
timeout: 5s
|
|
130
|
+
retries: 5
|
|
131
|
+
start_period: 30s
|
|
132
|
+
|
|
133
|
+
langgraph:
|
|
134
|
+
image: ghcr.io/bankung/agent-teams-langgraph:${AGENT_TEAMS_VERSION:-latest}
|
|
135
|
+
container_name: agent-teams-langgraph
|
|
136
|
+
depends_on:
|
|
137
|
+
db:
|
|
138
|
+
condition: service_healthy
|
|
139
|
+
api:
|
|
140
|
+
condition: service_healthy
|
|
141
|
+
environment:
|
|
142
|
+
DATABASE_URI: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/agent_teams?options=-c%20search_path=langgraph
|
|
143
|
+
LANGGRAPH_LLM_PROVIDER: ${LANGGRAPH_LLM_PROVIDER:-anthropic}
|
|
144
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
145
|
+
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
146
|
+
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-sonnet-4-6}
|
|
147
|
+
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o}
|
|
148
|
+
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-}
|
|
149
|
+
OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3.2}
|
|
150
|
+
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
|
151
|
+
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}
|
|
152
|
+
LANGGRAPH_DEEPSEEK_MODEL: ${LANGGRAPH_DEEPSEEK_MODEL:-deepseek-chat}
|
|
153
|
+
LANGGRAPH_DEEPSEEK_BASE_URL: ${LANGGRAPH_DEEPSEEK_BASE_URL:-https://api.deepseek.com}
|
|
154
|
+
LANGGRAPH_PROJECT_ID: ${LANGGRAPH_PROJECT_ID:-1}
|
|
155
|
+
LANGGRAPH_POLL_INTERVAL_SEC: ${LANGGRAPH_POLL_INTERVAL_SEC:-30}
|
|
156
|
+
LANGGRAPH_KANBAN_API_BASE: ${LANGGRAPH_KANBAN_API_BASE:-http://api:8456}
|
|
157
|
+
LANGGRAPH_CONTEXT_TOKEN_BUDGET: ${LANGGRAPH_CONTEXT_TOKEN_BUDGET:-60000}
|
|
158
|
+
HITL_DEMO_ENABLED: ${HITL_DEMO_ENABLED:-}
|
|
159
|
+
# REPO_ROOT intentionally absent: langgraph uses baked paths / LANGGRAPH_*
|
|
160
|
+
# vars and does not write to /repo. No volume mount is needed here.
|
|
161
|
+
ports:
|
|
162
|
+
- "${LANGGRAPH_PORT:-8465}:8000"
|
|
163
|
+
# No source bind-mount — code is baked into the image.
|
|
164
|
+
command: ["uvicorn", "graph:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
165
|
+
restart: unless-stopped
|
|
166
|
+
healthcheck:
|
|
167
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ok')"]
|
|
168
|
+
interval: 10s
|
|
169
|
+
timeout: 3s
|
|
170
|
+
retries: 3
|
|
171
|
+
start_period: 30s
|
|
172
|
+
|
|
173
|
+
volumes:
|
|
174
|
+
agent-teams-pgdata:
|
|
175
|
+
# Writable persistence for /repo inside the api container (REPO_ROOT=/repo).
|
|
176
|
+
# Allows scaffold/session/resource/audit writes without a source bind-mount.
|
|
177
|
+
agent-teams-repo-data:
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bankung/agent-teams",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI launcher for the agent-teams AI Kanban platform (pull pre-built images, or clone + build locally).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"agent-teams": "cli/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"cli/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"docker-compose.images.yml",
|
|
17
|
+
".env.example"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"agent-teams",
|
|
21
|
+
"kanban",
|
|
22
|
+
"claude",
|
|
23
|
+
"ai",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/bankung/agent-teams.git"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|