feat: parliament seat collaboration loop (0.9.19)

Seats publish via parliament_note, sync pushes on write, member join without
stealing the chair, and worker tools are hard-gated when parliament is active.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 11:29:38 +08:00
co-authored by Cursor
parent 1605d74f17
commit 812e3ee265
15 changed files with 532 additions and 83 deletions
+244 -3
View File
@@ -4,10 +4,12 @@
#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>
@@ -19,6 +21,75 @@
#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)
@@ -197,6 +268,7 @@ int cst_parliament_context_put(const char *text) {
m.revision++;
cst_parliament_save(&m);
}
cst_parliament_sync_request();
return 0;
}
@@ -216,6 +288,52 @@ char *cst_parliament_context_get(void) {
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));
@@ -311,6 +429,91 @@ char *cst_parliament_login(const char *password, const char *instance_id,
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);
}
@@ -378,13 +581,30 @@ int cst_parliament_session_ok(char *instance_id_out, size_t n) {
cst_parliament_meta_t m;
if (cst_parliament_load(&m) != 0)
return 0;
if (!m.chair_instance[0] || strcmp(m.chair_instance, iid) != 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)
@@ -406,9 +626,12 @@ int cst_parliament_apply_sync(unsigned long revision, const char *parliament_id,
if (chair) {
char local_sid[64] = {0};
int had = cst_parliament_session_ok(local_sid, sizeof(local_sid));
if (chair[0] && had && strcmp(local_sid, chair) != 0)
/* 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])
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);
}
@@ -468,6 +691,24 @@ char *cst_parliament_status_text(void) {
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);
}