Files
Hermit/src/main.c
T
WHCandCursor e610d70c30 fix: interactive Realm password, recoverable worker budget (0.9.15)
Prompt for Realm password when CST_REALM_PASSWORD is unset; map worker
request-gate exhaustion to a clear budget error with partial progress;
hard-skip identical tool failures after 3 tries. Add docs/parliament.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:53:36 +08:00

1021 lines
35 KiB
C

#include "cst_agent.h"
#include "cst_ext.h"
#include "cst_models.h"
#include "cst_parliament.h"
#include "cst_parliament_cli.h"
#include "cst_realm.h"
#include "cst_tools.h"
#include "cst_version.h"
#include "cst_webui.h"
#include "os_compat.h"
#include <curl/curl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#else
#include <unistd.h>
#endif
static void usage(const char *argv0) {
fprintf(stderr,
"Usage: %s [options] (works in the current directory)\n"
" -P, --provider NAME minimax | deepseek | openai | teamorouter\n"
" (last used)\n"
" -m, --model NAME model (last used for the provider)\n"
" -e, --effort LEVEL low | medium | high | max (last used)\n"
" -M, --mode NAME coding (default, code framed) | general\n"
" (documents, data, research; last used)\n"
" -w, --workspace DIR project directory (default: current)\n"
" -c, --continue reopen the last session (default)\n"
" -N, --new start a new session\n"
" -R, --resume ID open a session by id or unique prefix\n"
" -L, --list-sessions list sessions and exit\n"
" --migrate-store move sessions written by an older version\n"
" into one directory per session, and exit\n"
" --list-models [P] what is known about each provider's models\n"
" (built-in, <config>/models.json, GET /models)\n"
" -A, --approval MODE ask | auto (default: edits run, shell in\n"
" the sandbox or by safe rules) | yolo\n"
" -y, --yolo same as --approval yolo (this run only)\n"
" -C, --confirm LIST shell,write,net (default) | none\n"
" -r, --protocol P native-eos (default) | native-inc | ctl |\n"
" ctl-heredoc | xml\n"
" -T, --tools PROFILE default (all tools) | minimal (paper set)\n"
" -n, --max-steps N model calls per turn (30)\n"
" --orchestrate delegate edits and shell work to role workers\n"
" --no-orchestrate override workspace orchestration setting\n"
" --realm-new create Realm file keys (legacy/dev)\n"
" --realm-password-init create password-derived Realm (no sk on disk)\n"
" --login unlock (CST_REALM_PASSWORD or prompt); claim chair\n"
" --service run parliament service (discover+context sync)\n"
" --service-fg same as --service, stay in foreground\n"
" --service-stop stop the parliament background service\n"
" --new-project ID replace all parliament context for a new project\n"
" --realm-enroll ID enroll this instance under the Realm\n"
" --realm-label S display label for enroll/login\n"
" --peer-listen [N] accept one Realm peer handshake (port N)\n"
" --peer-dial H:N dial host:port and complete handshake\n"
" --discover-wait MS advertise and list same-Realm peers\n"
" -H, --headless -p TEXT run one prompt without the TUI\n"
" --web serve the UI over HTTP on 127.0.0.1:8787\n"
" --web-port N port (default 8787, 0 = pick a free one)\n"
" --web-host ADDR bind address (default 127.0.0.1; a\n"
" non-loopback address requires a token)\n"
" --web-token T require ?token=T / Authorization: Bearer T\n"
" --web-dev DIR serve the UI from DIR (development)\n"
" --web-open open the browser at startup\n"
" -a, --ask-key enter and save the provider's API key\n"
" -k, --api-key KEY use KEY for this run only (not saved)\n"
" -b, --api-base URL OpenAI-compatible base URL (…/v1)\n"
" -s, --session FILE explicit session file\n"
" -d, --demo-ctl local CTL parser demo (no API)\n"
" -V, --version print the version and exit\n"
"\n"
"API keys: the TUI asks for a missing key and saves it per provider\n"
" in the config dir (credentials, mode 0600); /key changes it.\n"
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n"
"Settings: provider, models, effort and mode are remembered in\n"
" settings.json next to the credentials.\n"
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n"
" CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n"
" CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n"
" CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n"
" CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
"TUI: type /help inside for commands and keys\n",
argv0);
}
static int parse_confirm(const char *s, cst_agent_config_t *cfg) {
cfg->confirm_shell = cfg->confirm_write = cfg->confirm_net = 0;
if (!s || strcmp(s, "none") == 0 || !s[0])
return 0;
char buf[128];
snprintf(buf, sizeof(buf), "%s", s);
for (char *tok = strtok(buf, ", "); tok; tok = strtok(NULL, ", ")) {
if (strcmp(tok, "shell") == 0)
cfg->confirm_shell = 1;
else if (strcmp(tok, "write") == 0)
cfg->confirm_write = 1;
else if (strcmp(tok, "net") == 0)
cfg->confirm_net = 1;
else if (strcmp(tok, "all") == 0)
cfg->confirm_shell = cfg->confirm_write = cfg->confirm_net = 1;
else
return -1;
}
return 0;
}
static void list_one_model(const char *provider, const char *id,
const char *where) {
cst_model_info_t mi;
cst_model_info(provider, id, &mi);
char tag[48], price[128] = "";
cst_model_info_label(&mi, tag, sizeof(tag));
if (mi.has_price)
snprintf(price, sizeof(price), " %s%g/%g/%g per 1M (miss/hit/out)",
mi.price.currency, mi.price.miss, mi.price.hit, mi.price.out);
printf(" %-40s %-18s %-8s%s [%s]\n", id, tag, where, price, mi.source);
}
/* --list-models [provider]: the merged registry, to check models.json. */
static int list_models(const char *only) {
char user[1100] = "";
cst_models_user_path(user, sizeof(user));
FILE *uf = user[0] ? fopen(user, "rb") : NULL;
printf("user file: %s%s\n", user[0] ? user : "(no config dir)",
uf ? "" : " (none)");
if (uf)
fclose(uf);
if (cst_models_load_error()[0])
printf("warning: %s\n", cst_models_load_error());
int shown = 0;
static cst_model_meta_t cached[1024];
for (int i = 0; i < cst_provider_count(); i++) {
const cst_provider_t *p = cst_provider_at(i);
if (only && only[0] && strcmp(p->name, only) != 0)
continue;
shown++;
printf("\n%s %s (%s; key %s)\n", p->name, p->api_base,
cst_provider_source(p), p->key_env[0] ? p->key_env : "-");
char listed[16][128];
int nl = cst_provider_models(p, listed, 16);
for (int k = 0; k < nl; k++)
list_one_model(p->name, listed[k],
strcmp(listed[k], p->model) == 0 ? "default" : "listed");
long age = 0;
int nc = cst_models_cache_load(p->name, cached, 1024, &age);
if (nc > 0) {
printf(" -- %d from GET /models, fetched %ldh ago%s\n", nc,
age / 3600, age >= CST_MODELS_CACHE_TTL ? " (stale)" : "");
for (int k = 0; k < nc; k++)
list_one_model(p->name, cached[k].id, "api");
} else if (!nl) {
printf(" (no models listed; the first one GET /models returns is "
"used)\n");
}
}
if (only && only[0] && !shown) {
fprintf(stderr, "unknown provider: %s\n", only);
return 1;
}
return 0;
}
static int list_sessions(const char *ws) {
static char ids[200][128];
int n = cst_agent_store_list(ws, ids, 200);
char cur[128] = "";
cst_agent_store_get_current(ws, cur, sizeof(cur));
if (n == 0) {
printf("no sessions under %s/.agent/sessions\n", ws);
return 0;
}
for (int i = 0; i < n; i++) {
cst_session_info_t in;
if (cst_agent_store_info(ws, ids[i], &in) != 0)
continue;
char when[32] = "?";
time_t t = (time_t)in.mtime;
struct tm tm;
os_localtime(&t, &tm);
strftime(when, sizeof(when), "%Y-%m-%d %H:%M", &tm);
printf("%c %-24s %s %4d msgs %s\n", strcmp(cur, ids[i]) == 0 ? '*' : ' ',
in.id, when, in.messages,
in.title[0] ? in.title : in.first_user);
}
return 0;
}
/* Move a store written by an older version into one directory per session
* (--migrate-store). Idempotent: what is already a directory is left alone. */
static int migrate_store(const char *ws) {
int moved = 0, failed = 0;
char err[256] = "";
if (cst_agent_store_migrate(ws, &moved, &failed, err, sizeof(err)) != 0) {
fprintf(stderr, "cannot migrate %s/.agent/sessions: %s\n", ws,
err[0] ? err : "unknown error");
return 1;
}
printf("migrated %d session(s) into %s/.agent/sessions/<id>/\n", moved, ws);
if (failed)
printf("%d session(s) could not be moved; their files are untouched\n",
failed);
if (moved == 0)
printf("nothing to move (already one directory per session)\n");
return failed ? 1 : 0;
}
/* Exact id or unique prefix → id. */
static int resolve_session_id(const char *ws, const char *want, char *out,
size_t out_sz) {
static char ids[500][128];
int n = cst_agent_store_list(ws, ids, 500), hit = -1;
for (int i = 0; i < n; i++) {
if (strcmp(ids[i], want) == 0) {
hit = i;
break;
}
if (strncmp(ids[i], want, strlen(want)) == 0) {
if (hit >= 0)
return -2;
hit = i;
}
}
if (hit < 0)
return -1;
snprintf(out, out_sz, "%s", ids[hit]);
return 0;
}
static int g_ready;
static ctl_tool_call_t g_call;
static void demo_on_ev(const ctl_event_t *ev, void *ud) {
(void)ud;
if (ev->type == CTL_EV_TEXT)
fwrite(ev->text, 1, ev->text_len, stdout);
if (ev->type == CTL_EV_CALL_READY) {
snprintf(g_call.id, sizeof(g_call.id), "%s", ev->call->id);
snprintf(g_call.name, sizeof(g_call.name), "%s", ev->call->name);
for (int i = 0; i < g_call.nargs; i++) {
free(g_call.args[i].value);
g_call.args[i].value = NULL;
}
g_call.nargs = 0;
for (int i = 0; i < ev->call->nargs; i++) {
snprintf(g_call.args[i].key, sizeof(g_call.args[i].key), "%s",
ev->call->args[i].key);
g_call.args[i].value_len = ev->call->args[i].value_len;
g_call.args[i].value = malloc(ev->call->args[i].value_len + 1);
memcpy(g_call.args[i].value, ev->call->args[i].value,
ev->call->args[i].value_len);
g_call.args[i].value[ev->call->args[i].value_len] = 0;
g_call.nargs++;
}
g_ready = 1;
}
}
static int demo_ctl_impl(const char *workspace) {
printf("=== demo CTL (byte-at-a-time feed) ===\n");
g_ready = 0;
memset(&g_call, 0, sizeof(g_call));
ctl_parser_t *p = ctl_parser_create(demo_on_ev, NULL);
const char *stream =
"Reading README.\n"
"call t1 read_file\n"
"arg path 9\n"
"README.md"
"end\n";
for (size_t i = 0; stream[i]; i++)
ctl_parser_feed(p, &stream[i], 1);
if (ctl_parser_finish(p) != CTL_OK || !g_ready) {
fprintf(stderr, "demo: CTL failed\n");
ctl_parser_destroy(p);
return 1;
}
printf("\npending_peak=%zu\n", ctl_parser_pending_peak(p));
ctl_parser_destroy(p);
cst_tool_result_t r = {0};
cst_tool_dispatch(workspace, &g_call, &r);
printf("[tool result exit=%d]\n%.*s\n", r.exit_code, (int)r.out_len,
r.out ? r.out : "");
cst_tool_result_free(&r);
for (int i = 0; i < g_call.nargs; i++)
free(g_call.args[i].value);
return 0;
}
/* 0..65535, digits only: atoi turned "70000" into port 4464, "abc" into 0. */
static int parse_port(const char *s, int *out) {
char *end = NULL;
long v = strtol(s, &end, 10);
if (!s[0] || *end || v < 0 || v > 65535)
return -1;
*out = (int)v;
return 0;
}
int main(int argc, char **argv) {
if (os_console_init() != 0)
fprintf(stderr, "warning: the process code page is not UTF-8 (Windows 10 "
"1903 or later is required); non-ASCII text may break\n");
os_set_prog_path(argv[0]);
os_temp_dir_export();
curl_global_init(CURL_GLOBAL_DEFAULT);
cst_agent_config_t cfg = {0};
char cwd[4096];
#ifdef _WIN32
if (!_getcwd(cwd, (int)sizeof(cwd)))
#else
if (!getcwd(cwd, sizeof(cwd)))
#endif
snprintf(cwd, sizeof(cwd), ".");
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", cwd);
cfg.session_path[0] = 0; /* filled after workspace opts via .agent/store */
cfg.api_base[0] = 0;
cfg.api_key[0] = 0;
cfg.model[0] = 0;
cfg.protocol = CST_PROTO_NATIVE_EOS;
const char *env_proto = getenv("CST_PROTOCOL");
if (env_proto && env_proto[0] &&
cst_protocol_parse(env_proto, &cfg.protocol) != 0) {
fprintf(stderr, "unknown CST_PROTOCOL=%s\n", env_proto);
return 1;
}
cfg.max_steps = CST_DEFAULT_MAX_STEPS;
const char *env_steps = getenv("CST_MAX_STEPS");
if (env_steps && atoi(env_steps) > 0)
cfg.max_steps = atoi(env_steps);
cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP;
const char *env_cap = getenv("CST_TOOL_OUT_CAP");
if (env_cap && atol(env_cap) > 0)
cfg.tool_out_cap = (size_t)atol(env_cap);
const char *provider_name = getenv("CST_PROVIDER");
cfg.headless = 0;
cfg.confirm_shell = 1;
cfg.confirm_write = 1;
cfg.confirm_net = 1;
cfg.auto_approve = 0;
cfg.tool_profile = CST_TOOLS_DEFAULT;
const char *env_tools = getenv("CST_TOOLS");
if (env_tools && env_tools[0] &&
cst_tool_profile_parse(env_tools, &cfg.tool_profile) != 0) {
fprintf(stderr, "unknown CST_TOOLS=%s (default | minimal)\n", env_tools);
return 1;
}
if (getenv("CST_AUTO_APPROVE"))
cfg.auto_approve = 1;
const char *env_confirm = getenv("CST_CONFIRM");
if (env_confirm && parse_confirm(env_confirm, &cfg) != 0) {
fprintf(stderr, "bad CST_CONFIRM=%s (shell,write,net | none)\n",
env_confirm);
return 1;
}
int approval = -1;
const char *env_appr = getenv("CST_APPROVAL");
if (env_appr && env_appr[0] && cst_approval_parse(env_appr, &approval) != 0) {
fprintf(stderr, "bad CST_APPROVAL=%s (ask | auto | yolo)\n", env_appr);
return 1;
}
if (cfg.auto_approve)
approval = CST_APPROVAL_YOLO;
int do_demo = 0;
int force_ask_key = 0;
int session_explicit = 0;
const char *prompt = NULL;
int new_session = 0, do_list = 0, do_migrate = 0;
const char *resume_id = NULL;
int do_realm_new = 0, do_realm_enroll = 0, do_peer_listen = 0, do_peer_dial = 0,
do_discover = 0, do_realm_pw_init = 0, do_login = 0, do_new_project = 0,
do_service = 0, do_service_fg = 0, do_service_stop = 0;
const char *realm_enroll_id = NULL, *realm_label = NULL;
const char *peer_dial_spec = NULL, *parliament_instance = NULL;
const char *new_project_id = NULL;
int peer_listen_port = 0, discover_wait_ms = 3000, discover_peer_port = 9100;
/* WebUI (--web): a second front end on the same engine. */
int web_mode = 0;
cst_webui_opts_t web;
memset(&web, 0, sizeof(web));
web.port = CST_WEBUI_DEFAULT_PORT; /* 0 would mean "pick a free one" */
const char *env = getenv("CST_WEB_PORT");
if (env && env[0] && parse_port(env, &web.port) != 0) {
fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env);
return 1;
}
web.host = getenv("CST_WEB_HOST");
web.token = getenv("CST_WEB_TOKEN");
web.root = getenv("CST_WEB_ROOT");
static struct option opts[] = {
{"workspace", required_argument, 0, 'w'},
{"session", required_argument, 0, 's'},
{"api-base", required_argument, 0, 'b'},
{"api-key", required_argument, 0, 'k'},
{"model", required_argument, 0, 'm'},
{"provider", required_argument, 0, 'P'},
{"protocol", required_argument, 0, 'r'},
{"max-steps", required_argument, 0, 'n'},
{"tools", required_argument, 0, 'T'},
{"headless", no_argument, 0, 'H'},
{"prompt", required_argument, 0, 'p'},
{"demo-ctl", no_argument, 0, 'd'},
{"ask-key", no_argument, 0, 'a'},
{"new", no_argument, 0, 'N'},
{"continue", no_argument, 0, 'c'},
{"resume", required_argument, 0, 'R'},
{"list-sessions", no_argument, 0, 'L'},
{"migrate-store", no_argument, 0, 0x107},
{"yolo", no_argument, 0, 'y'},
{"approval", required_argument, 0, 'A'},
{"confirm", required_argument, 0, 'C'},
{"effort", required_argument, 0, 'e'},
{"mode", required_argument, 0, 'M'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{"list-models", optional_argument, 0, 0x100},
{"web", no_argument, 0, 0x101},
{"web-port", required_argument, 0, 0x102},
{"web-host", required_argument, 0, 0x103},
{"web-token", required_argument, 0, 0x104},
{"web-dev", required_argument, 0, 0x105},
{"web-open", no_argument, 0, 0x106},
{"orchestrate", no_argument, 0, 0x108},
{"no-orchestrate", no_argument, 0, 0x109},
{"realm-new", no_argument, 0, 0x110},
{"realm-enroll", required_argument, 0, 0x111},
{"realm-label", required_argument, 0, 0x112},
{"peer-listen", optional_argument, 0, 0x113},
{"peer-dial", required_argument, 0, 0x114},
{"discover-wait", required_argument, 0, 0x115},
{"instance", required_argument, 0, 0x116},
{"peer-port", required_argument, 0, 0x117},
{"realm-password-init", no_argument, 0, 0x118},
{"login", no_argument, 0, 0x119},
{"new-project", required_argument, 0, 0x11a},
{"service", no_argument, 0, 0x11b},
{"service-fg", no_argument, 0, 0x11c},
{"service-stop", no_argument, 0, 0x11d},
{0, 0, 0, 0}};
int c;
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:",
opts, NULL)) != -1) {
switch (c) {
case 0x108:
cfg.orchestrate_override = 1;
break;
case 0x109:
cfg.orchestrate_override = -1;
break;
case 0x110:
do_realm_new = 1;
break;
case 0x111:
do_realm_enroll = 1;
realm_enroll_id = optarg;
break;
case 0x112:
realm_label = optarg;
break;
case 0x113:
do_peer_listen = 1;
peer_listen_port = optarg && optarg[0] ? atoi(optarg) : 0;
break;
case 0x114:
do_peer_dial = 1;
peer_dial_spec = optarg;
break;
case 0x115:
do_discover = 1;
discover_wait_ms = atoi(optarg);
if (discover_wait_ms <= 0)
discover_wait_ms = 3000;
break;
case 0x116:
parliament_instance = optarg;
break;
case 0x117:
discover_peer_port = atoi(optarg);
if (discover_peer_port <= 0)
discover_peer_port = 9100;
break;
case 0x118:
do_realm_pw_init = 1;
break;
case 0x119:
do_login = 1;
break;
case 0x11a:
do_new_project = 1;
new_project_id = optarg;
break;
case 0x11b:
do_service = 1;
break;
case 0x11c:
do_service_fg = 1;
break;
case 0x11d:
do_service_stop = 1;
break;
case 'w':
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
break;
case 's':
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s", optarg);
session_explicit = 1;
break;
case 'b':
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s", optarg);
break;
case 'k':
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", optarg);
break;
case 'm':
snprintf(cfg.model, sizeof(cfg.model), "%s", optarg);
break;
case 'P':
provider_name = optarg;
break;
case 'r':
if (cst_protocol_parse(optarg, &cfg.protocol) != 0) {
fprintf(stderr, "unknown protocol: %s\n", optarg);
curl_global_cleanup();
return 1;
}
break;
case 'n':
cfg.max_steps = atoi(optarg);
if (cfg.max_steps <= 0)
cfg.max_steps = CST_DEFAULT_MAX_STEPS;
break;
case 'T':
if (cst_tool_profile_parse(optarg, &cfg.tool_profile) != 0) {
fprintf(stderr, "unknown tool profile: %s (default | minimal)\n",
optarg);
curl_global_cleanup();
return 1;
}
break;
case 'H':
cfg.headless = 1;
break;
case 'p':
prompt = optarg;
break;
case 'd':
do_demo = 1;
break;
case 'a':
force_ask_key = 1;
break;
case 'N':
new_session = 1;
break;
case 'c':
new_session = 0;
resume_id = NULL;
break;
case 'R':
resume_id = optarg;
break;
case 'L':
do_list = 1;
break;
case 'y':
approval = CST_APPROVAL_YOLO;
break;
case 'A':
if (cst_approval_parse(optarg, &approval) != 0) {
fprintf(stderr, "bad --approval %s (ask | auto | yolo)\n", optarg);
curl_global_cleanup();
return 1;
}
break;
case 'C':
if (parse_confirm(optarg, &cfg) != 0) {
fprintf(stderr, "bad --confirm %s (shell,write,net | none)\n", optarg);
curl_global_cleanup();
return 1;
}
break;
case 'e':
if (!cst_effort_valid(optarg)) {
fprintf(stderr, "bad --effort %s (low | medium | high | max)\n",
optarg);
curl_global_cleanup();
return 1;
}
snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg);
break;
case 'M':
if (!cst_mode_valid(optarg)) {
fprintf(stderr, "bad --mode %s (coding | general)\n", optarg);
curl_global_cleanup();
return 1;
}
snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg);
break;
case 'h':
usage(argv[0]);
curl_global_cleanup();
return 0;
case 0x100: {
const char *only = optarg;
if (!only && optind < argc && argv[optind][0] != '-')
only = argv[optind++];
int rc = list_models(only);
curl_global_cleanup();
return rc;
}
case 0x101:
web_mode = 1;
break;
case 0x102:
if (parse_port(optarg, &web.port) != 0) {
fprintf(stderr, "--web-port %s is not a port (0-65535)\n", optarg);
curl_global_cleanup();
return 1;
}
web_mode = 1;
break;
case 0x103:
web.host = optarg;
web_mode = 1;
break;
case 0x104:
web.token = optarg;
web_mode = 1;
break;
case 0x105:
web.root = optarg;
web_mode = 1;
break;
case 0x106:
web.open_browser = 1;
web_mode = 1;
break;
case 0x107:
do_migrate = 1;
break;
case 'V':
printf("cst-agent %s\n", CST_VERSION);
curl_global_cleanup();
return 0;
default:
usage(argv[0]);
curl_global_cleanup();
return 1;
}
}
if (do_migrate) {
int rc = migrate_store(cfg.workspace_root);
curl_global_cleanup();
return rc;
}
if (do_realm_new) {
int rc = cst_parliament_cli_realm_new();
curl_global_cleanup();
return rc;
}
if (do_realm_pw_init) {
const char *pw = getenv("CST_REALM_PASSWORD");
int rc = cst_parliament_cli_realm_password_init(pw);
curl_global_cleanup();
return rc;
}
if (do_login) {
const char *pw = getenv("CST_REALM_PASSWORD");
const char *iid = parliament_instance ? parliament_instance
: getenv("CST_INSTANCE_ID");
if (!iid || !iid[0])
iid = "chair-local";
int rc = cst_parliament_cli_login(pw, iid, realm_label, discover_peer_port);
curl_global_cleanup();
return rc;
}
if (do_new_project) {
int rc = cst_parliament_cli_new_project(new_project_id);
curl_global_cleanup();
return rc;
}
if (do_service_stop) {
int rc = cst_parliament_cli_service_stop();
curl_global_cleanup();
return rc;
}
if (do_service || do_service_fg) {
const char *pw = getenv("CST_REALM_PASSWORD");
const char *iid = parliament_instance ? parliament_instance
: getenv("CST_INSTANCE_ID");
if (!iid || !iid[0])
iid = "chair-local";
int rc = cst_parliament_cli_service(pw, iid, discover_peer_port,
do_service_fg);
curl_global_cleanup();
return rc;
}
if (do_realm_enroll) {
int rc = cst_parliament_cli_realm_enroll(realm_enroll_id, realm_label);
curl_global_cleanup();
return rc;
}
if (do_peer_listen) {
const char *iid = parliament_instance ? parliament_instance : realm_enroll_id;
if (!iid)
iid = getenv("CST_INSTANCE_ID");
int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid);
curl_global_cleanup();
return rc;
}
if (do_peer_dial) {
const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID");
char host[256];
int port = 0;
host[0] = 0;
if (peer_dial_spec) {
const char *colon = strrchr(peer_dial_spec, ':');
if (colon && colon > peer_dial_spec) {
size_t hl = (size_t)(colon - peer_dial_spec);
if (hl >= sizeof(host))
hl = sizeof(host) - 1;
memcpy(host, peer_dial_spec, hl);
host[hl] = 0;
port = atoi(colon + 1);
}
}
int rc = cst_parliament_cli_peer_dial(host, port, iid);
curl_global_cleanup();
return rc;
}
if (do_discover) {
const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID");
int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms);
curl_global_cleanup();
return rc;
}
/* Password Realm: interactive/agent open requires a chair login session. */
{
char rdir[OS_PATH_MAX];
if (cst_realm_dir(rdir, sizeof(rdir)) == 0 &&
cst_realm_is_password_mode(rdir) && !cst_parliament_session_ok(NULL, 0)) {
char pwbuf[256];
const char *pw = getenv("CST_REALM_PASSWORD");
if ((!pw || !pw[0]) && isatty(0) &&
cst_prompt_secret("Realm password to open agent (hidden): ", pwbuf,
sizeof(pwbuf)) == 0)
pw = pwbuf;
if (pw && pw[0]) {
const char *iid = parliament_instance ? parliament_instance
: getenv("CST_INSTANCE_ID");
if (!iid || !iid[0])
iid = "chair-local";
char err[256];
char *hand =
cst_parliament_login(pw, iid, realm_label, err, sizeof(err));
if (!hand) {
fprintf(stderr, "%s\n", err[0] ? err : "login required");
if (pw == pwbuf)
memset(pwbuf, 0, sizeof(pwbuf));
curl_global_cleanup();
return 1;
}
free(hand);
if (cst_parliament_daemon_start(pw, iid, discover_peer_port, err,
sizeof(err)) != 0)
fprintf(stderr, "warning: parliament service: %s\n",
err[0] ? err : "start failed");
if (pw == pwbuf)
memset(pwbuf, 0, sizeof(pwbuf));
} else {
fprintf(stderr,
"password Realm active: run --login (or set "
"CST_REALM_PASSWORD) before opening the agent\n");
curl_global_cleanup();
return 1;
}
}
}
if (do_list) {
int rc = list_sessions(cfg.workspace_root);
curl_global_cleanup();
return rc;
}
/* Explicit option > environment > last used (settings.json) > preset. */
cst_settings_t saved;
cst_settings_load(&saved);
if ((!provider_name || !provider_name[0]) &&
cst_provider_find(saved.provider))
provider_name = saved.provider;
const cst_provider_t *prov = cst_provider_default();
if (provider_name && provider_name[0]) {
prov = cst_provider_find(provider_name);
if (!prov) {
fprintf(stderr, "unknown provider: %s\n", provider_name);
curl_global_cleanup();
return 1;
}
}
snprintf(cfg.provider, sizeof(cfg.provider), "%s", prov->name);
if (!cfg.effort[0]) {
const char *e = getenv("CST_EFFORT");
if (!cst_effort_valid(e))
e = saved.effort;
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
}
if (!cfg.mode[0]) {
const char *m = getenv("CST_MODE");
if (!cst_mode_valid(m))
m = saved.mode;
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
}
if (!cfg.api_base[0]) {
const char *env_base = getenv("CST_API_BASE");
if (!env_base || !env_base[0])
env_base = getenv("OPENAI_BASE_URL");
if (!env_base || !env_base[0])
env_base = getenv("OPENAI_API_BASE");
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s",
env_base && env_base[0] ? env_base : prov->api_base);
}
if (!cfg.model[0]) {
const char *env_model = getenv("CST_MODEL");
if (!env_model || !env_model[0])
env_model = getenv("OPENAI_MODEL");
if (!env_model || !env_model[0])
env_model = cst_settings_model(&saved, prov->name);
snprintf(cfg.model, sizeof(cfg.model), "%s",
env_model && env_model[0] ? env_model : prov->model);
}
/* --api-key > saved key > provider env > CST_API_KEY; the TUI asks for
* a missing key itself. */
if (!cfg.api_key[0] && !force_ask_key &&
cst_creds_resolve(prov->name, cfg.api_key, sizeof(cfg.api_key), NULL) !=
0) {
const char *k = getenv("CST_API_KEY");
if (k && k[0])
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", k);
}
/* trim trailing slash on api_base */
size_t alen = strlen(cfg.api_base);
while (alen > 0 && cfg.api_base[alen - 1] == '/')
cfg.api_base[--alen] = 0;
/* Option > environment > last picked (settings.json) > auto. */
if (approval < 0 && cst_approval_parse(saved.approval, &approval) != 0)
approval = CST_DEFAULT_APPROVAL;
cfg.approval = approval;
cfg.auto_approve = approval == CST_APPROVAL_YOLO;
/* Headless defaults to auto-approve shell/write (no interactive confirm). */
if (cfg.headless)
cfg.auto_approve = 1;
/* Default session under <workspace>/.agent/sessions/ (OpenCode-like). */
if (!session_explicit && (new_session || resume_id)) {
char id[128];
int rc = 0;
if (new_session) {
rc = cst_agent_store_ensure(cfg.workspace_root) != 0 ||
cst_agent_store_new_id(id, sizeof(id)) != 0
? -1
: 0;
} else {
rc = resolve_session_id(cfg.workspace_root, resume_id, id, sizeof(id));
if (rc != 0)
fprintf(stderr, "%s session: %s (see --list-sessions)\n",
rc == -2 ? "ambiguous" : "no such", resume_id);
}
if (rc != 0 || cst_agent_store_path_for_id(cfg.workspace_root, id,
cfg.session_path,
sizeof(cfg.session_path)) != 0) {
curl_global_cleanup();
return 1;
}
cst_agent_store_set_current(cfg.workspace_root, id);
fprintf(stderr, "session %s%s\n", id, new_session ? " (new)" : "");
} else if (!session_explicit) {
char id[128];
if (cst_agent_store_resolve_default(cfg.workspace_root, cfg.session_path,
sizeof(cfg.session_path), id,
sizeof(id)) != 0) {
fprintf(stderr, "failed to init .agent/sessions under %s\n",
cfg.workspace_root);
curl_global_cleanup();
return 1;
}
fprintf(stderr, "session %s\n", id);
}
if (do_demo) {
int rc = demo_ctl_impl(cfg.workspace_root);
curl_global_cleanup();
return rc;
}
int need_console_key =
force_ask_key || (!cfg.api_key[0] && cfg.headless && isatty(0));
if (need_console_key) {
if (cst_creds_prompt_console(cfg.provider, cfg.api_key,
sizeof(cfg.api_key)) != 0) {
fprintf(stderr, "no API key entered\n");
curl_global_cleanup();
return 1;
}
char dir[1024] = "the config directory";
os_config_dir(dir, sizeof(dir));
if (cst_creds_save(cfg.provider, cfg.api_key) != 0)
fprintf(stderr, "warning: failed to save credentials\n");
else
fprintf(stderr, "saved the %s API key to %s/credentials\n", cfg.provider,
dir);
/* --ask-key alone: save and exit */
if (force_ask_key && !cfg.headless && !prompt) {
curl_global_cleanup();
return 0;
}
}
if (!cfg.api_key[0] && (cfg.headless || web_mode)) {
fprintf(stderr,
"no API key for %s: start cst-agent once to enter it, or run "
"cst-agent --ask-key --provider %s\n",
cfg.provider, cfg.provider);
curl_global_cleanup();
return 1;
}
/* Router presets have no fixed model: take the first one the API lists
* (the TUI does the same after its /models probe). */
if (!cfg.model[0] && (cfg.headless || web_mode)) {
static cst_model_meta_t list[512];
char err[256] = "";
long age = 0;
int n = cst_models_cache_load(cfg.provider, list, 1, &age);
if (n <= 0 || age >= CST_MODELS_CACHE_TTL) {
int r = cst_llm_list_models_meta(cfg.api_base, cfg.api_key, list, 512,
err, sizeof(err));
if (r >= 0)
cst_models_cache_save(cfg.provider, cfg.api_base, list, r);
if (r >= 0 || n <= 0)
n = r;
}
if (n > 0) {
snprintf(cfg.model, sizeof(cfg.model), "%s", list[0].id);
} else {
fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider,
err[0] ? err : "the API listed none");
curl_global_cleanup();
return 1;
}
}
fprintf(stderr,
"using %s model=%s effort=%s protocol=%s tools=%s max_steps=%d\n",
cfg.api_base, cfg.model, cfg.effort, cst_protocol_name(cfg.protocol),
cst_tool_profile_name(cfg.tool_profile), cfg.max_steps);
{
char ext[256];
cst_extensions_load(&cfg, ext, sizeof(ext));
if (ext[0] && cfg.headless)
fprintf(stderr, "extensions: %s\n", ext);
if (cfg.headless)
cst_extensions_wait(stderr);
}
cst_agent_t *agent = cst_agent_create(&cfg);
if (!agent) {
fprintf(stderr, "failed to create agent (session path?)\n");
curl_global_cleanup();
return 1;
}
int rc = 0;
if (web_mode) {
web.prompt = prompt; /* --web -p "...": run one turn at startup */
rc = cst_webui_run(agent, &web);
} else if (cfg.headless) {
if (!prompt) {
fprintf(stderr, "--headless requires --prompt\n");
rc = 1;
} else {
char err[512];
if (cst_agent_run_turn(agent, prompt, err, sizeof(err)) != 0) {
fprintf(stderr, "turn failed: %s\n", err);
rc = 1;
} else {
cst_session_t *s = cst_agent_session(agent);
for (int i = 0; i < cst_session_count(s); i++) {
const cst_message_t *m = cst_session_get(s, i);
printf("[%d role=%d] %s\n", i, (int)m->role,
m->content ? m->content : "");
}
}
}
} else {
rc = cst_tui_run(agent);
}
cst_agent_destroy(agent);
cst_extensions_unload();
curl_global_cleanup();
return rc;
}