Password Realm no longer auto-enables parliament (keeps task/worker tools); finished jobs fan out to agent notes + TUI; raise PAR_MAX/grep caps and tag truncations. Co-authored-by: Cursor <cursoragent@cursor.com>
716 lines
22 KiB
C
716 lines
22 KiB
C
#ifndef _DEFAULT_SOURCE
|
|
#define _DEFAULT_SOURCE
|
|
#endif
|
|
#include "cst_parliament.h"
|
|
#include "cJSON.h"
|
|
#include "cst_agent.h"
|
|
#include "cst_discover.h"
|
|
#include "cst_util.h"
|
|
#include "monocypher.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#define PARL_CONTEXT_MAX (256 * 1024)
|
|
|
|
static int sync_want_path(char *out, size_t n) {
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
|
return -1;
|
|
return os_path_join(out, n, dir, "sync.want");
|
|
}
|
|
|
|
void cst_parliament_sync_request(void) {
|
|
char path[OS_PATH_MAX];
|
|
if (sync_want_path(path, sizeof(path)) != 0)
|
|
return;
|
|
FILE *f = fopen(path, "wb");
|
|
if (!f)
|
|
return;
|
|
fputs("1\n", f);
|
|
fclose(f);
|
|
}
|
|
|
|
int cst_parliament_sync_pending(void) {
|
|
char path[OS_PATH_MAX];
|
|
if (sync_want_path(path, sizeof(path)) != 0)
|
|
return 0;
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f)
|
|
return 0;
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
void cst_parliament_sync_ack(void) {
|
|
char path[OS_PATH_MAX];
|
|
if (sync_want_path(path, sizeof(path)) == 0)
|
|
remove(path);
|
|
}
|
|
|
|
int cst_parliament_default_instance(char *out, size_t n) {
|
|
if (!out || n < 2)
|
|
return -1;
|
|
const char *env = getenv("CST_INSTANCE_ID");
|
|
if (env && env[0]) {
|
|
snprintf(out, n, "%s", env);
|
|
return 0;
|
|
}
|
|
char host[128] = {0};
|
|
#ifdef _WIN32
|
|
DWORD hn = (DWORD)sizeof(host);
|
|
if (!GetComputerNameA(host, &hn) || !host[0])
|
|
snprintf(host, sizeof(host), "%s", "seat");
|
|
#else
|
|
if (gethostname(host, sizeof(host)) != 0 || !host[0])
|
|
snprintf(host, sizeof(host), "%s", "seat");
|
|
host[sizeof(host) - 1] = 0;
|
|
#endif
|
|
size_t j = 0;
|
|
for (size_t i = 0; host[i] && j + 1 < n; i++) {
|
|
unsigned char c = (unsigned char)host[i];
|
|
if (isalnum(c) || c == '-' || c == '_')
|
|
out[j++] = (char)c;
|
|
else if (c == '.' || c == ' ')
|
|
out[j++] = '-';
|
|
}
|
|
out[j] = 0;
|
|
if (!out[0])
|
|
snprintf(out, n, "%s", "seat");
|
|
return 0;
|
|
}
|
|
|
|
int cst_parliament_path(char *out, size_t n) {
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
|
return -1;
|
|
if (os_mkdir_p(dir, 0700) != 0)
|
|
return -1;
|
|
return os_path_join(out, n, dir, "parliament.json");
|
|
}
|
|
|
|
static int context_path(char *out, size_t n) {
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
|
return -1;
|
|
return os_path_join(out, n, dir, "parliament.context");
|
|
}
|
|
|
|
int cst_parliament_load(cst_parliament_meta_t *m) {
|
|
if (!m)
|
|
return -1;
|
|
memset(m, 0, sizeof(*m));
|
|
char path[OS_PATH_MAX];
|
|
if (cst_parliament_path(path, sizeof(path)) != 0)
|
|
return -1;
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
snprintf(m->status, sizeof(m->status), "%s", "idle");
|
|
return 0;
|
|
}
|
|
cst_buf_t b = {0};
|
|
char chunk[4096];
|
|
size_t n;
|
|
while ((n = fread(chunk, 1, sizeof(chunk), f)) && b.len < 256 * 1024)
|
|
cst_buf_append(&b, chunk, n);
|
|
fclose(f);
|
|
cJSON *o = cJSON_Parse(b.buf ? b.buf : "{}");
|
|
cst_buf_free(&b);
|
|
if (!o)
|
|
return -1;
|
|
const cJSON *v;
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "parliament_id");
|
|
if (cJSON_IsString(v))
|
|
snprintf(m->parliament_id, sizeof(m->parliament_id), "%s", v->valuestring);
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "project_id");
|
|
if (cJSON_IsString(v))
|
|
snprintf(m->project_id, sizeof(m->project_id), "%s", v->valuestring);
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "chair_instance");
|
|
if (cJSON_IsString(v))
|
|
snprintf(m->chair_instance, sizeof(m->chair_instance), "%s",
|
|
v->valuestring);
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "chair_device");
|
|
if (cJSON_IsString(v))
|
|
snprintf(m->chair_device, sizeof(m->chair_device), "%s", v->valuestring);
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "revision");
|
|
if (cJSON_IsNumber(v))
|
|
m->revision = (unsigned long)v->valuedouble;
|
|
v = cJSON_GetObjectItemCaseSensitive(o, "status");
|
|
if (cJSON_IsString(v))
|
|
snprintf(m->status, sizeof(m->status), "%s", v->valuestring);
|
|
else
|
|
snprintf(m->status, sizeof(m->status), "%s", "idle");
|
|
cJSON_Delete(o);
|
|
return 0;
|
|
}
|
|
|
|
int cst_parliament_save(const cst_parliament_meta_t *m) {
|
|
if (!m)
|
|
return -1;
|
|
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
|
|
if (cst_parliament_path(path, sizeof(path)) != 0)
|
|
return -1;
|
|
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
|
return -1;
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "parliament_id", m->parliament_id);
|
|
cJSON_AddStringToObject(o, "project_id", m->project_id);
|
|
cJSON_AddStringToObject(o, "chair_instance", m->chair_instance);
|
|
cJSON_AddStringToObject(o, "chair_device", m->chair_device);
|
|
cJSON_AddNumberToObject(o, "revision", (double)m->revision);
|
|
cJSON_AddStringToObject(o, "status", m->status);
|
|
char *txt = cJSON_PrintUnformatted(o);
|
|
cJSON_Delete(o);
|
|
if (!txt)
|
|
return -1;
|
|
FILE *f = fopen(tmp, "wb");
|
|
int ok = 0;
|
|
if (f) {
|
|
ok = fputs(txt, f) >= 0 && fputc('\n', f) >= 0;
|
|
if (fclose(f) != 0)
|
|
ok = 0;
|
|
}
|
|
free(txt);
|
|
if (!ok) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
if (os_rename_replace(tmp, path) != 0) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
char *cst_parliament_device_context(void) {
|
|
cst_buf_t b = {0};
|
|
char host[128] = "unknown", cwd[OS_PATH_MAX] = ".";
|
|
#ifdef _WIN32
|
|
DWORD n = sizeof(host);
|
|
GetComputerNameA(host, &n);
|
|
_getcwd(cwd, (int)sizeof(cwd));
|
|
cst_buf_printf(&b, "os=windows\nhost=%s\ncwd=%s\npid=%d\n", host, cwd,
|
|
(int)GetCurrentProcessId());
|
|
#else
|
|
gethostname(host, sizeof(host));
|
|
if (!getcwd(cwd, sizeof(cwd)))
|
|
snprintf(cwd, sizeof(cwd), ".");
|
|
cst_buf_printf(&b, "os=unix\nhost=%s\ncwd=%s\npid=%d\n", host, cwd,
|
|
(int)getpid());
|
|
#ifdef __linux__
|
|
cst_buf_puts(&b, "platform=linux\n");
|
|
#elif defined(__APPLE__)
|
|
cst_buf_puts(&b, "platform=macos\n");
|
|
#endif
|
|
#endif
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
int cst_parliament_claim_chair(const char *instance_id,
|
|
const char *device_label) {
|
|
if (!instance_id || !instance_id[0])
|
|
return -1;
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) != 0)
|
|
return -1;
|
|
if (!m.parliament_id[0]) {
|
|
snprintf(m.parliament_id, sizeof(m.parliament_id), "parl-%ld",
|
|
(long)time(NULL));
|
|
snprintf(m.status, sizeof(m.status), "%s", "open");
|
|
}
|
|
/* Single chair: logging-in device always takes the chair (user will). */
|
|
snprintf(m.chair_instance, sizeof(m.chair_instance), "%s", instance_id);
|
|
snprintf(m.chair_device, sizeof(m.chair_device), "%s",
|
|
device_label ? device_label : "");
|
|
m.revision++;
|
|
snprintf(m.status, sizeof(m.status), "%s", "open");
|
|
return cst_parliament_save(&m);
|
|
}
|
|
|
|
static int context_write_file(const char *text) {
|
|
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
|
|
if (context_path(path, sizeof(path)) != 0)
|
|
return -1;
|
|
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
|
return -1;
|
|
FILE *f = fopen(tmp, "wb");
|
|
if (!f)
|
|
return -1;
|
|
int ok = fputs(text ? text : "", f) >= 0;
|
|
if (fclose(f) != 0)
|
|
ok = 0;
|
|
if (!ok) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
if (os_rename_replace(tmp, path) != 0) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int cst_parliament_context_put(const char *text) {
|
|
if (context_write_file(text) != 0)
|
|
return -1;
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) == 0) {
|
|
m.revision++;
|
|
cst_parliament_save(&m);
|
|
}
|
|
cst_parliament_sync_request();
|
|
return 0;
|
|
}
|
|
|
|
char *cst_parliament_context_get(void) {
|
|
char path[OS_PATH_MAX];
|
|
if (context_path(path, sizeof(path)) != 0)
|
|
return strdup("");
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f)
|
|
return strdup("");
|
|
cst_buf_t b = {0};
|
|
char chunk[4096];
|
|
size_t n;
|
|
while ((n = fread(chunk, 1, sizeof(chunk), f)) && b.len < 2 * 1024 * 1024)
|
|
cst_buf_append(&b, chunk, n);
|
|
fclose(f);
|
|
return b.buf ? cst_buf_steal(&b) : strdup("");
|
|
}
|
|
|
|
int cst_parliament_context_append(const char *seat_id, const char *kind,
|
|
const char *body) {
|
|
if (!body || !body[0])
|
|
return -1;
|
|
const char *seat = seat_id && seat_id[0] ? seat_id : "unknown";
|
|
const char *k = kind && kind[0] ? kind : "note";
|
|
char *prev = cst_parliament_context_get();
|
|
cst_buf_t b = {0};
|
|
if (prev && prev[0]) {
|
|
cst_buf_puts(&b, prev);
|
|
if (prev[strlen(prev) - 1] != '\n')
|
|
cst_buf_puts(&b, "\n");
|
|
}
|
|
free(prev);
|
|
time_t now = time(NULL);
|
|
struct tm tm;
|
|
#ifdef _WIN32
|
|
gmtime_s(&tm, &now);
|
|
#else
|
|
gmtime_r(&now, &tm);
|
|
#endif
|
|
char ts[40];
|
|
strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%MZ", &tm);
|
|
cst_buf_printf(&b, "---\nseat=%s kind=%s at=%s\n", seat, k, ts);
|
|
cst_buf_puts(&b, body);
|
|
if (body[strlen(body) - 1] != '\n')
|
|
cst_buf_puts(&b, "\n");
|
|
/* Keep the newest tail when the shared blob grows too large. */
|
|
if (b.len > PARL_CONTEXT_MAX) {
|
|
size_t keep = PARL_CONTEXT_MAX - 64;
|
|
const char *start = b.buf + (b.len - keep);
|
|
while (start > b.buf && *start != '\n')
|
|
start++;
|
|
if (*start == '\n')
|
|
start++;
|
|
cst_buf_t slim = {0};
|
|
cst_buf_puts(&slim, "…(earlier parliament notes truncated)\n");
|
|
cst_buf_append(&slim, start, (size_t)(b.buf + b.len - start));
|
|
cst_buf_free(&b);
|
|
b = slim;
|
|
}
|
|
int rc = cst_parliament_context_put(b.buf);
|
|
cst_buf_free(&b);
|
|
return rc;
|
|
}
|
|
|
|
int cst_parliament_new_project(const char *project_id) {
|
|
cst_parliament_meta_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
snprintf(m.parliament_id, sizeof(m.parliament_id), "parl-%ld",
|
|
(long)time(NULL));
|
|
snprintf(m.project_id, sizeof(m.project_id), "%s",
|
|
project_id && project_id[0] ? project_id : "default");
|
|
snprintf(m.status, sizeof(m.status), "%s", "open");
|
|
m.revision = 1;
|
|
/* Keep chair empty until next login claims it. */
|
|
cst_parliament_session_clear();
|
|
(void)cst_parliament_daemon_stop();
|
|
if (cst_parliament_save(&m) != 0)
|
|
return -1;
|
|
return context_write_file("");
|
|
}
|
|
|
|
char *cst_parliament_login(const char *password, const char *instance_id,
|
|
const char *device_label, char *err, size_t err_sz) {
|
|
if (err && err_sz)
|
|
err[0] = 0;
|
|
if (!password || !password[0] || !instance_id || !instance_id[0]) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "password and instance id required");
|
|
return NULL;
|
|
}
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "realm dir missing");
|
|
return NULL;
|
|
}
|
|
if (!cst_realm_is_password_mode(dir)) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "realm is not password mode; run --realm-password-init");
|
|
return NULL;
|
|
}
|
|
cst_realm_keys_t keys;
|
|
if (cst_realm_unlock_password(dir, password, &keys) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "login failed (bad password)");
|
|
return NULL;
|
|
}
|
|
/* Ensure this device has an enrolled instance cert. */
|
|
cst_realm_cert_t cert;
|
|
uint8_t sk[64];
|
|
if (cst_realm_instance_load(instance_id, &cert, sk) != 0) {
|
|
uint8_t seed[32];
|
|
if (cst_realm_enroll(&keys, instance_id, device_label, 0, &cert, seed, sk) !=
|
|
0 ||
|
|
cst_realm_instance_save(instance_id, &cert, seed) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "enroll instance failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
return NULL;
|
|
}
|
|
}
|
|
cst_parliament_meta_t before;
|
|
cst_parliament_load(&before);
|
|
char prev_chair[64];
|
|
snprintf(prev_chair, sizeof(prev_chair), "%s", before.chair_instance);
|
|
if (cst_parliament_claim_chair(instance_id, device_label) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "claim chair failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
return NULL;
|
|
}
|
|
char *ctx = cst_parliament_context_get();
|
|
char *dev = cst_parliament_device_context();
|
|
cst_buf_t hand = {0};
|
|
cst_buf_puts(&hand, "LOGIN_OK\n");
|
|
cst_buf_printf(&hand, "chair=%s\n", instance_id);
|
|
if (prev_chair[0] && strcmp(prev_chair, instance_id) != 0)
|
|
cst_buf_printf(&hand, "handover_from=%s\n", prev_chair);
|
|
else
|
|
cst_buf_puts(&hand, "handover_from=\n");
|
|
cst_buf_printf(&hand, "device_context=\n%s", dev ? dev : "");
|
|
cst_buf_puts(&hand, "--- parliament_context ---\n");
|
|
cst_buf_puts(&hand, ctx ? ctx : "");
|
|
free(ctx);
|
|
free(dev);
|
|
if (cst_parliament_session_write(instance_id) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "session ticket write failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
crypto_wipe(sk, sizeof(sk));
|
|
cst_buf_free(&hand);
|
|
return NULL;
|
|
}
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
crypto_wipe(sk, sizeof(sk));
|
|
cst_parliament_sync_request();
|
|
return cst_buf_steal(&hand);
|
|
}
|
|
|
|
char *cst_parliament_seat_join(const char *password, const char *instance_id,
|
|
const char *device_label, char *err,
|
|
size_t err_sz) {
|
|
if (err && err_sz)
|
|
err[0] = 0;
|
|
if (!password || !password[0] || !instance_id || !instance_id[0]) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "password and instance id required");
|
|
return NULL;
|
|
}
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "realm dir missing");
|
|
return NULL;
|
|
}
|
|
if (!cst_realm_is_password_mode(dir)) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "realm is not password mode");
|
|
return NULL;
|
|
}
|
|
cst_realm_keys_t keys;
|
|
if (cst_realm_unlock_password(dir, password, &keys) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "join failed (bad password)");
|
|
return NULL;
|
|
}
|
|
cst_realm_cert_t cert;
|
|
uint8_t sk[64];
|
|
if (cst_realm_instance_load(instance_id, &cert, sk) != 0) {
|
|
uint8_t seed[32];
|
|
if (cst_realm_enroll(&keys, instance_id, device_label, 0, &cert, seed, sk) !=
|
|
0 ||
|
|
cst_realm_instance_save(instance_id, &cert, seed) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "enroll instance failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
return NULL;
|
|
}
|
|
}
|
|
cst_parliament_meta_t m;
|
|
cst_parliament_load(&m);
|
|
int as_chair = 0;
|
|
if (!m.chair_instance[0]) {
|
|
if (cst_parliament_claim_chair(instance_id, device_label) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "claim vacant chair failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
crypto_wipe(sk, sizeof(sk));
|
|
return NULL;
|
|
}
|
|
as_chair = 1;
|
|
} else if (strcmp(m.chair_instance, instance_id) == 0)
|
|
as_chair = 1;
|
|
cst_parliament_load(&m);
|
|
if (cst_parliament_session_write(instance_id) != 0) {
|
|
if (err && err_sz)
|
|
snprintf(err, err_sz, "session ticket write failed");
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
crypto_wipe(sk, sizeof(sk));
|
|
return NULL;
|
|
}
|
|
char *ctx = cst_parliament_context_get();
|
|
char *dev = cst_parliament_device_context();
|
|
cst_buf_t hand = {0};
|
|
cst_buf_puts(&hand, "JOIN_OK\n");
|
|
cst_buf_printf(&hand, "seat=%s\n", instance_id);
|
|
cst_buf_printf(&hand, "role=%s\n", as_chair ? "chair" : "member");
|
|
cst_buf_printf(&hand, "chair=%s\n",
|
|
m.chair_instance[0] ? m.chair_instance : "");
|
|
cst_buf_printf(&hand, "device_context=\n%s", dev ? dev : "");
|
|
cst_buf_puts(&hand, "--- parliament_context ---\n");
|
|
cst_buf_puts(&hand, ctx ? ctx : "");
|
|
free(ctx);
|
|
free(dev);
|
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
|
crypto_wipe(sk, sizeof(sk));
|
|
return cst_buf_steal(&hand);
|
|
}
|
|
|
|
static int session_path(char *out, size_t n) {
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
|
return -1;
|
|
return os_path_join(out, n, dir, "chair.session");
|
|
}
|
|
|
|
int cst_parliament_session_write(const char *instance_id) {
|
|
if (!instance_id || !instance_id[0])
|
|
return -1;
|
|
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
|
|
if (session_path(path, sizeof(path)) != 0)
|
|
return -1;
|
|
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
|
return -1;
|
|
FILE *f = fopen(tmp, "wb");
|
|
if (!f)
|
|
return -1;
|
|
int ok = fprintf(f, "instance=%s\nissued=%ld\n", instance_id,
|
|
(long)time(NULL)) > 0;
|
|
if (fclose(f) != 0)
|
|
ok = 0;
|
|
if (!ok) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
if (os_rename_replace(tmp, path) != 0) {
|
|
remove(tmp);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int cst_parliament_session_ok(char *instance_id_out, size_t n) {
|
|
char path[OS_PATH_MAX];
|
|
if (session_path(path, sizeof(path)) != 0)
|
|
return 0;
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f)
|
|
return 0;
|
|
char line[256], iid[64] = {0};
|
|
long issued = 0;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (!strncmp(line, "instance=", 9)) {
|
|
const char *p = line + 9;
|
|
size_t i = 0;
|
|
while (p[i] && p[i] != '\n' && p[i] != '\r' && i + 1 < sizeof(iid)) {
|
|
iid[i] = p[i];
|
|
i++;
|
|
}
|
|
iid[i] = 0;
|
|
} else if (!strncmp(line, "issued=", 7))
|
|
issued = atol(line + 7);
|
|
}
|
|
fclose(f);
|
|
if (!iid[0] || issued <= 0)
|
|
return 0;
|
|
/* Ticket valid for 24h of wall clock. */
|
|
long now = (long)time(NULL);
|
|
if (now < issued || now - issued > 86400)
|
|
return 0;
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) != 0)
|
|
return 0;
|
|
/* Any enrolled seat may hold a ticket while the parliament is open.
|
|
* Chair exclusivity is tracked in meta, not by invalidating member seats. */
|
|
if (!m.status[0] || !strcmp(m.status, "idle") || !strcmp(m.status, "dissolved"))
|
|
return 0;
|
|
cst_realm_cert_t cert;
|
|
uint8_t sk[64];
|
|
if (cst_realm_instance_load(iid, &cert, sk) != 0)
|
|
return 0;
|
|
crypto_wipe(sk, sizeof(sk));
|
|
if (instance_id_out && n)
|
|
snprintf(instance_id_out, n, "%s", iid);
|
|
return 1;
|
|
}
|
|
|
|
int cst_parliament_is_chair(void) {
|
|
char sid[64] = {0};
|
|
if (!cst_parliament_session_ok(sid, sizeof(sid)))
|
|
return 0;
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) != 0 || !m.chair_instance[0])
|
|
return 0;
|
|
return strcmp(m.chair_instance, sid) == 0;
|
|
}
|
|
|
|
void cst_parliament_session_clear(void) {
|
|
char path[OS_PATH_MAX];
|
|
if (session_path(path, sizeof(path)) == 0)
|
|
remove(path);
|
|
}
|
|
|
|
int cst_parliament_apply_sync(unsigned long revision, const char *parliament_id,
|
|
const char *project_id, const char *chair,
|
|
const char *context) {
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) != 0)
|
|
return -1;
|
|
if (revision <= m.revision)
|
|
return 0;
|
|
if (parliament_id && parliament_id[0])
|
|
snprintf(m.parliament_id, sizeof(m.parliament_id), "%s", parliament_id);
|
|
if (project_id)
|
|
snprintf(m.project_id, sizeof(m.project_id), "%s", project_id);
|
|
if (chair) {
|
|
char local_sid[64] = {0};
|
|
int had = cst_parliament_session_ok(local_sid, sizeof(local_sid));
|
|
/* Only demote/clear when this seat previously held the chair. */
|
|
if (chair[0] && had && m.chair_instance[0] &&
|
|
!strcmp(local_sid, m.chair_instance) && strcmp(local_sid, chair) != 0)
|
|
cst_parliament_session_clear();
|
|
if (!chair[0] && had && m.chair_instance[0] &&
|
|
!strcmp(local_sid, m.chair_instance))
|
|
cst_parliament_session_clear();
|
|
snprintf(m.chair_instance, sizeof(m.chair_instance), "%s", chair);
|
|
}
|
|
m.revision = revision;
|
|
snprintf(m.status, sizeof(m.status), "%s", "open");
|
|
if (context_write_file(context ? context : "") != 0)
|
|
return -1;
|
|
return cst_parliament_save(&m);
|
|
}
|
|
|
|
int cst_parliament_is_active(void) {
|
|
/* Solo (legacy_workers) is the default for single-machine execution.
|
|
* Password Realm alone no longer forces parliament — that starved task /
|
|
* worker_* / runtime tools. Opt in via CST_COLLABORATION / settings, or
|
|
* when an open parliament meta already exists. */
|
|
const char *mode = getenv("CST_COLLABORATION");
|
|
if (mode && mode[0])
|
|
return strcmp(mode, "parliament") == 0;
|
|
cst_settings_t s;
|
|
if (cst_settings_load(&s) == 0 && s.collaboration[0])
|
|
return strcmp(s.collaboration, "parliament") == 0;
|
|
cst_parliament_meta_t m;
|
|
if (cst_parliament_load(&m) == 0 && m.status[0] &&
|
|
strcmp(m.status, "idle") != 0 && strcmp(m.status, "dissolved") != 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
char *cst_parliament_status_text(void) {
|
|
cst_buf_t b = {0};
|
|
char dir[OS_PATH_MAX];
|
|
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
|
cst_buf_puts(&b, "parliament (no realm dir)\n");
|
|
return cst_buf_steal(&b);
|
|
}
|
|
int pw = cst_realm_is_password_mode(dir);
|
|
cst_parliament_meta_t m;
|
|
memset(&m, 0, sizeof(m));
|
|
cst_parliament_load(&m);
|
|
char sid[64] = {0};
|
|
int sess = cst_parliament_session_ok(sid, sizeof(sid));
|
|
int svc_pid = 0, svc_port = 0;
|
|
char svc_id[64] = {0};
|
|
int svc = cst_parliament_daemon_info(&svc_pid, svc_id, sizeof(svc_id),
|
|
&svc_port) == 0;
|
|
cst_buf_printf(&b, "realm %s\n", pw ? "password" : "file-keys/legacy");
|
|
cst_buf_printf(&b, "parliament %s project=%s rev=%lu\n",
|
|
m.status[0] ? m.status : "idle",
|
|
m.project_id[0] ? m.project_id : "-",
|
|
m.revision);
|
|
cst_buf_printf(&b, "chair %s%s%s\n",
|
|
m.chair_instance[0] ? m.chair_instance : "(none)",
|
|
m.chair_device[0] ? " @ " : "", m.chair_device);
|
|
cst_buf_printf(&b, "login %s%s%s\n", sess ? "ok" : "required",
|
|
sess && sid[0] ? " as " : "", sess ? sid : "");
|
|
if (svc)
|
|
cst_buf_printf(&b, "service running pid=%d id=%s port=%d\n", svc_pid,
|
|
svc_id[0] ? svc_id : "-", svc_port);
|
|
else
|
|
cst_buf_puts(&b, "service stopped\n");
|
|
cst_buf_printf(&b, "role %s\n",
|
|
sess ? (cst_parliament_is_chair() ? "chair" : "member")
|
|
: "logged-out");
|
|
cst_buf_puts(&b, "peer_sync sealed (X25519 + XChaCha20-Poly1305)\n");
|
|
{
|
|
cst_peer_info_t peers[16];
|
|
int np = cst_discover_local_list(NULL, peers, 16);
|
|
if (np > 0) {
|
|
cst_buf_puts(&b, "peers\n");
|
|
for (int i = 0; i < np; i++) {
|
|
int is_chair = m.chair_instance[0] &&
|
|
!strcmp(peers[i].instance_id, m.chair_instance);
|
|
cst_buf_printf(&b, " - %s%s port=%d\n", peers[i].instance_id,
|
|
is_chair ? " (chair)" : "", peers[i].port);
|
|
}
|
|
} else {
|
|
cst_buf_puts(&b, "peers (none discovered yet)\n");
|
|
}
|
|
}
|
|
return cst_buf_steal(&b);
|
|
}
|