feat: password Realm, single-chair login, shared parliament context (0.9.12)
Derive Realm keys from a user password (Argon2id, no private key on disk). Login claims the sole chair with handover; peers share parliament context; --new-project clears it. CLI peer/discover helpers and Docker alpine recipe included; headless empty-answer retries raised for unattended runs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
.git
|
||||||
|
result
|
||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
dist/
|
||||||
|
third_party/
|
||||||
|
backups/
|
||||||
|
reports/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
cst-agent
|
||||||
|
tests/test_*
|
||||||
|
!tests/test_*.c
|
||||||
|
app/
|
||||||
|
.cursor/
|
||||||
|
*.apk
|
||||||
|
*.tar.gz
|
||||||
+3
-1
@@ -124,6 +124,8 @@ set(CST_CORE_SOURCES
|
|||||||
src/realm.c
|
src/realm.c
|
||||||
src/discover.c
|
src/discover.c
|
||||||
src/peer_auth.c
|
src/peer_auth.c
|
||||||
|
src/parliament.c
|
||||||
|
src/parliament_cli.c
|
||||||
src/attach.c
|
src/attach.c
|
||||||
src/clipboard.c
|
src/clipboard.c
|
||||||
src/pdf_text.c
|
src/pdf_text.c
|
||||||
@@ -389,7 +391,7 @@ set(_CST_CORE_TESTS test_llm_body test_prefix_stable test_sse_decode
|
|||||||
test_undo test_config test_models test_model_registry test_sandbox
|
test_undo test_config test_models test_model_registry test_sandbox
|
||||||
test_keymap test_shell_compact test_skills
|
test_keymap test_shell_compact test_skills
|
||||||
test_plugins test_mcp test_subagent test_runtime test_realm test_discover
|
test_plugins test_mcp test_subagent test_runtime test_realm test_discover
|
||||||
test_peer_auth test_lowil
|
test_peer_auth test_parliament test_lowil
|
||||||
test_win_utf8 test_os_dir test_tmpdir
|
test_win_utf8 test_os_dir test_tmpdir
|
||||||
test_tool_meta test_embed)
|
test_tool_meta test_embed)
|
||||||
if(CST_BUILD_WEBUI)
|
if(CST_BUILD_WEBUI)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ CORE_SRC = \
|
|||||||
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
|
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
|
||||||
src/pdf_text.c \
|
src/pdf_text.c \
|
||||||
src/session.c src/runtime.c src/worktree.c src/realm.c src/discover.c \
|
src/session.c src/runtime.c src/worktree.c src/realm.c src/discover.c \
|
||||||
src/peer_auth.c \
|
src/peer_auth.c src/parliament.c src/parliament_cli.c \
|
||||||
src/tools/tools.c \
|
src/tools/tools.c \
|
||||||
src/tools/patch.c \
|
src/tools/patch.c \
|
||||||
src/tools/registry.c \
|
src/tools/registry.c \
|
||||||
@@ -177,6 +177,7 @@ TESTS = \
|
|||||||
tests/test_realm \
|
tests/test_realm \
|
||||||
tests/test_discover \
|
tests/test_discover \
|
||||||
tests/test_peer_auth \
|
tests/test_peer_auth \
|
||||||
|
tests/test_parliament \
|
||||||
tests/test_lowil \
|
tests/test_lowil \
|
||||||
tests/test_win_utf8 \
|
tests/test_win_utf8 \
|
||||||
tests/test_os_dir \
|
tests/test_os_dir \
|
||||||
|
|||||||
@@ -497,6 +497,8 @@ typedef struct {
|
|||||||
char effort[16];
|
char effort[16];
|
||||||
char mode[16]; /* coding | general, "" = not chosen yet */
|
char mode[16]; /* coding | general, "" = not chosen yet */
|
||||||
char approval[8]; /* "" = not chosen yet */
|
char approval[8]; /* "" = not chosen yet */
|
||||||
|
/* legacy_workers | parliament — parliament replaces in-process subagents. */
|
||||||
|
char collaboration[24];
|
||||||
int show_thinking; /* TUI: Thought blocks start expanded */
|
int show_thinking; /* TUI: Thought blocks start expanded */
|
||||||
/* workspaces whose .agent/plugins and .agent/mcp.json may run */
|
/* workspaces whose .agent/plugins and .agent/mcp.json may run */
|
||||||
int ntrusted;
|
int ntrusted;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#ifndef CST_PARLIAMENT_H
|
||||||
|
#define CST_PARLIAMENT_H
|
||||||
|
|
||||||
|
#include "cst_realm.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Parliament is one shared whole across agents. Password unlock claims the
|
||||||
|
* single chair on the logging-in device; peers sync the same context. A new
|
||||||
|
* project replaces all parliament context. */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char parliament_id[64];
|
||||||
|
char project_id[64];
|
||||||
|
char chair_instance[64];
|
||||||
|
char chair_device[128];
|
||||||
|
unsigned long revision;
|
||||||
|
char status[32]; /* idle | open | dissolved */
|
||||||
|
} cst_parliament_meta_t;
|
||||||
|
|
||||||
|
int cst_parliament_path(char *out, size_t n);
|
||||||
|
int cst_parliament_load(cst_parliament_meta_t *m);
|
||||||
|
int cst_parliament_save(const cst_parliament_meta_t *m);
|
||||||
|
|
||||||
|
/* Login: unlock Realm with password, enroll/ensure instance, claim chair,
|
||||||
|
* return handover summary (malloc'd) or NULL. */
|
||||||
|
char *cst_parliament_login(const char *password, const char *instance_id,
|
||||||
|
const char *device_label, char *err, size_t err_sz);
|
||||||
|
|
||||||
|
/* Only one chair: previous chair is demoted; handover notes left in meta. */
|
||||||
|
int cst_parliament_claim_chair(const char *instance_id, const char *device_label);
|
||||||
|
|
||||||
|
/* Probe local environment as device-private context (malloc'd text). */
|
||||||
|
char *cst_parliament_device_context(void);
|
||||||
|
|
||||||
|
/* New project: wipe shared parliament context; keep Realm keys. */
|
||||||
|
int cst_parliament_new_project(const char *project_id);
|
||||||
|
|
||||||
|
/* Append/replace synchronized context blob (all agents share this file). */
|
||||||
|
int cst_parliament_context_put(const char *text);
|
||||||
|
char *cst_parliament_context_get(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef CST_PARLIAMENT_CLI_H
|
||||||
|
#define CST_PARLIAMENT_CLI_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Standalone parliament/Realm ops for Docker and unattended smoke tests.
|
||||||
|
* Returns a process exit code (0 = success). */
|
||||||
|
int cst_parliament_cli_realm_new(void);
|
||||||
|
int cst_parliament_cli_realm_password_init(const char *password);
|
||||||
|
int cst_parliament_cli_login(const char *password, const char *instance_id,
|
||||||
|
const char *device_label);
|
||||||
|
int cst_parliament_cli_new_project(const char *project_id);
|
||||||
|
int cst_parliament_cli_realm_enroll(const char *instance_id, const char *label);
|
||||||
|
int cst_parliament_cli_peer_listen(int port, const char *instance_id);
|
||||||
|
int cst_parliament_cli_peer_dial(const char *host, int port,
|
||||||
|
const char *instance_id);
|
||||||
|
int cst_parliament_cli_discover(const char *instance_id, int peer_port,
|
||||||
|
int wait_ms);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -36,6 +36,15 @@ int cst_realm_load(const char *dir, cst_realm_keys_t *out);
|
|||||||
/* 32 hex chars (blake2b-128 of pk). */
|
/* 32 hex chars (blake2b-128 of pk). */
|
||||||
int cst_realm_fingerprint(const uint8_t pk[32], char out[33]);
|
int cst_realm_fingerprint(const uint8_t pk[32], char out[33]);
|
||||||
|
|
||||||
|
/* Password-string Realm: Argon2id(password, salt) → seed. Private key is not
|
||||||
|
* stored; only salt + public.key live on disk. Unlock derives sk in memory. */
|
||||||
|
int cst_realm_derive_password(cst_realm_keys_t *out, const char *password,
|
||||||
|
const uint8_t salt[16]);
|
||||||
|
int cst_realm_init_password(const char *dir, const char *password);
|
||||||
|
int cst_realm_unlock_password(const char *dir, const char *password,
|
||||||
|
cst_realm_keys_t *out);
|
||||||
|
int cst_realm_is_password_mode(const char *dir);
|
||||||
|
|
||||||
int cst_realm_enroll(const cst_realm_keys_t *realm, const char *instance_id,
|
int cst_realm_enroll(const cst_realm_keys_t *realm, const char *instance_id,
|
||||||
const char *label, int64_t not_after, cst_realm_cert_t *cert,
|
const char *label, int64_t not_after, cst_realm_cert_t *cert,
|
||||||
uint8_t instance_seed[32], uint8_t instance_sk[64]);
|
uint8_t instance_seed[32], uint8_t instance_sk[64]);
|
||||||
@@ -52,6 +61,12 @@ int cst_realm_challenge_check(const cst_realm_cert_t *cert, const void *chal,
|
|||||||
int cst_realm_revoke_add(const char *dir, const char *instance_id);
|
int cst_realm_revoke_add(const char *dir, const char *instance_id);
|
||||||
int cst_realm_revoked(const char *dir, const char *instance_id);
|
int cst_realm_revoked(const char *dir, const char *instance_id);
|
||||||
|
|
||||||
|
/* Persist an enrolled instance under <realm>/instances/<id>/. */
|
||||||
|
int cst_realm_instance_save(const char *instance_id, const cst_realm_cert_t *cert,
|
||||||
|
const uint8_t instance_seed[32]);
|
||||||
|
int cst_realm_instance_load(const char *instance_id, cst_realm_cert_t *cert,
|
||||||
|
uint8_t instance_sk[64]);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#define CST_VERSION_MAJOR 0
|
#define CST_VERSION_MAJOR 0
|
||||||
#define CST_VERSION_MINOR 9
|
#define CST_VERSION_MINOR 9
|
||||||
#define CST_VERSION_PATCH 11
|
#define CST_VERSION_PATCH 12
|
||||||
|
|
||||||
#define CST_VERSION_STR_(x) #x
|
#define CST_VERSION_STR_(x) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Build inside Alpine so the binary does not depend on the host Nix store.
|
||||||
|
FROM alpine:latest AS build
|
||||||
|
RUN apk add --no-cache build-base curl-dev ncurses-dev pkgconf python3
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
# Skip WebUI asset embedding quirks in minimal images if needed; keep default on.
|
||||||
|
RUN make -j"$(nproc)" cst-agent
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk add --no-cache libcurl ncurses-libs ca-certificates
|
||||||
|
COPY --from=build /src/cst-agent /usr/local/bin/cst-agent
|
||||||
|
ENV XDG_CONFIG_HOME=/realm-config
|
||||||
|
WORKDIR /work
|
||||||
|
ENTRYPOINT ["cst-agent"]
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Real Docker smoke: shared Realm volume, chair listens, member dials.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
IMG="${PARLIAMENT_IMAGE:-cst-parliament:local}"
|
||||||
|
NET="${PARLIAMENT_NET:-cst-parliament-net}"
|
||||||
|
VOL="${PARLIAMENT_VOL:-cst-parliament-realm}"
|
||||||
|
LISTEN_PORT=9454
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
echo "== docker build (alpine) =="
|
||||||
|
docker build -f scripts/Dockerfile.parliament -t "$IMG" .
|
||||||
|
|
||||||
|
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET"
|
||||||
|
docker volume inspect "$VOL" >/dev/null 2>&1 || docker volume create "$VOL"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
docker rm -f cst-parl-a cst-parl-b >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
cleanup
|
||||||
|
docker volume rm "$VOL" >/dev/null 2>&1 || true
|
||||||
|
docker volume create "$VOL" >/dev/null
|
||||||
|
|
||||||
|
echo "== realm + enroll on shared volume =="
|
||||||
|
docker run --rm -v "$VOL:/realm-config" -e XDG_CONFIG_HOME=/realm-config "$IMG" --realm-new
|
||||||
|
docker run --rm -v "$VOL:/realm-config" -e XDG_CONFIG_HOME=/realm-config "$IMG" \
|
||||||
|
--realm-enroll chair-a --realm-label chair
|
||||||
|
docker run --rm -v "$VOL:/realm-config" -e XDG_CONFIG_HOME=/realm-config "$IMG" \
|
||||||
|
--realm-enroll member-b --realm-label member
|
||||||
|
|
||||||
|
echo "== chair listens =="
|
||||||
|
docker run -d --name cst-parl-a --network "$NET" \
|
||||||
|
-v "$VOL:/realm-config" -e XDG_CONFIG_HOME=/realm-config \
|
||||||
|
"$IMG" --peer-listen="$LISTEN_PORT" --instance chair-a
|
||||||
|
|
||||||
|
for i in $(seq 1 50); do
|
||||||
|
if docker logs cst-parl-a 2>&1 | grep -q "peer_listen port="; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
docker logs cst-parl-a 2>&1 | tail -8
|
||||||
|
|
||||||
|
echo "== member dials =="
|
||||||
|
docker run --rm --name cst-parl-b --network "$NET" \
|
||||||
|
-v "$VOL:/realm-config" -e XDG_CONFIG_HOME=/realm-config \
|
||||||
|
"$IMG" --peer-dial="cst-parl-a:${LISTEN_PORT}" --instance member-b
|
||||||
|
|
||||||
|
echo "== chair result =="
|
||||||
|
for i in $(seq 1 50); do
|
||||||
|
if docker logs cst-parl-a 2>&1 | grep -q "peer_ok"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
docker logs cst-parl-a 2>&1 | grep peer_ok
|
||||||
|
|
||||||
|
echo "docker_parliament_test: OK"
|
||||||
+13
-5
@@ -3251,11 +3251,19 @@ static int run_turn_once(cst_agent_t *a, const char *user_text,
|
|||||||
}
|
}
|
||||||
turn_free(&t);
|
turn_free(&t);
|
||||||
|
|
||||||
if (empty_retries < 2 && step + 1 < a->cfg.max_steps) {
|
/* Headless / unattended turns allow more empty-answer retries so a
|
||||||
empty_retries++;
|
* flaky gateway does not end the turn blank. CST_EMPTY_RETRIES overrides. */
|
||||||
nudge = NUDGE_EMPTY;
|
{
|
||||||
hook_status(&t, "empty response — retrying...");
|
int empty_cap = a->cfg.headless ? 4 : 2;
|
||||||
continue;
|
const char *env_er = getenv("CST_EMPTY_RETRIES");
|
||||||
|
if (env_er && atoi(env_er) > 0)
|
||||||
|
empty_cap = atoi(env_er);
|
||||||
|
if (empty_retries < empty_cap && step + 1 < a->cfg.max_steps) {
|
||||||
|
empty_retries++;
|
||||||
|
nudge = NUDGE_EMPTY;
|
||||||
|
hook_status(&t, "empty response — retrying...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Last resort: surface the latest tool output so the turn isn't blank. */
|
/* Last resort: surface the latest tool output so the turn isn't blank. */
|
||||||
if (tools_ran) {
|
if (tools_ran) {
|
||||||
|
|||||||
+135
@@ -1,6 +1,7 @@
|
|||||||
#include "cst_agent.h"
|
#include "cst_agent.h"
|
||||||
#include "cst_ext.h"
|
#include "cst_ext.h"
|
||||||
#include "cst_models.h"
|
#include "cst_models.h"
|
||||||
|
#include "cst_parliament_cli.h"
|
||||||
#include "cst_tools.h"
|
#include "cst_tools.h"
|
||||||
#include "cst_version.h"
|
#include "cst_version.h"
|
||||||
#include "cst_webui.h"
|
#include "cst_webui.h"
|
||||||
@@ -47,6 +48,15 @@ static void usage(const char *argv0) {
|
|||||||
" -n, --max-steps N model calls per turn (30)\n"
|
" -n, --max-steps N model calls per turn (30)\n"
|
||||||
" --orchestrate delegate edits and shell work to role workers\n"
|
" --orchestrate delegate edits and shell work to role workers\n"
|
||||||
" --no-orchestrate override workspace orchestration setting\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 with password; this device becomes chair\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"
|
" -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 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-port N port (default 8787, 0 = pick a free one)\n"
|
||||||
@@ -363,6 +373,12 @@ int main(int argc, char **argv) {
|
|||||||
const char *prompt = NULL;
|
const char *prompt = NULL;
|
||||||
int new_session = 0, do_list = 0, do_migrate = 0;
|
int new_session = 0, do_list = 0, do_migrate = 0;
|
||||||
const char *resume_id = NULL;
|
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;
|
||||||
|
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. */
|
/* WebUI (--web): a second front end on the same engine. */
|
||||||
int web_mode = 0;
|
int web_mode = 0;
|
||||||
cst_webui_opts_t web;
|
cst_webui_opts_t web;
|
||||||
@@ -412,6 +428,17 @@ int main(int argc, char **argv) {
|
|||||||
{"web-open", no_argument, 0, 0x106},
|
{"web-open", no_argument, 0, 0x106},
|
||||||
{"orchestrate", no_argument, 0, 0x108},
|
{"orchestrate", no_argument, 0, 0x108},
|
||||||
{"no-orchestrate", no_argument, 0, 0x109},
|
{"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},
|
||||||
{0, 0, 0, 0}};
|
{0, 0, 0, 0}};
|
||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:",
|
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:",
|
||||||
@@ -423,6 +450,48 @@ int main(int argc, char **argv) {
|
|||||||
case 0x109:
|
case 0x109:
|
||||||
cfg.orchestrate_override = -1;
|
cfg.orchestrate_override = -1;
|
||||||
break;
|
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 'w':
|
case 'w':
|
||||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
||||||
break;
|
break;
|
||||||
@@ -580,6 +649,72 @@ int main(int argc, char **argv) {
|
|||||||
return rc;
|
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);
|
||||||
|
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_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;
|
||||||
|
}
|
||||||
|
|
||||||
if (do_list) {
|
if (do_list) {
|
||||||
int rc = list_sessions(cfg.workspace_root);
|
int rc = list_sessions(cfg.workspace_root);
|
||||||
curl_global_cleanup();
|
curl_global_cleanup();
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#ifndef _DEFAULT_SOURCE
|
||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#endif
|
||||||
|
#include "cst_parliament.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "cst_util.h"
|
||||||
|
#include "monocypher.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_context_put(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;
|
||||||
|
}
|
||||||
|
cst_parliament_meta_t m;
|
||||||
|
if (cst_parliament_load(&m) == 0) {
|
||||||
|
m.revision++;
|
||||||
|
cst_parliament_save(&m);
|
||||||
|
}
|
||||||
|
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_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. */
|
||||||
|
if (cst_parliament_save(&m) != 0)
|
||||||
|
return -1;
|
||||||
|
return cst_parliament_context_put("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
crypto_wipe(keys.seed, sizeof(keys.seed));
|
||||||
|
crypto_wipe(keys.sk, sizeof(keys.sk));
|
||||||
|
crypto_wipe(sk, sizeof(sk));
|
||||||
|
return cst_buf_steal(&hand);
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
#ifndef _DEFAULT_SOURCE
|
||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#endif
|
||||||
|
#include "cst_parliament_cli.h"
|
||||||
|
#include "cst_discover.h"
|
||||||
|
#include "cst_net.h"
|
||||||
|
#include "cst_parliament.h"
|
||||||
|
#include "cst_peer.h"
|
||||||
|
#include "cst_realm.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void sleep_ms(int ms) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
Sleep((DWORD)ms);
|
||||||
|
#else
|
||||||
|
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_realm_new(void) {
|
||||||
|
char dir[OS_PATH_MAX], fp[33];
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
||||||
|
fprintf(stderr, "cannot resolve realm directory\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_realm_keys_t k;
|
||||||
|
if (cst_realm_generate(&k) != 0 || cst_realm_save(dir, &k) != 0) {
|
||||||
|
fprintf(stderr, "cannot create realm keys in %s\n", dir);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_realm_fingerprint(k.pk, fp);
|
||||||
|
printf("realm_created dir=%s fingerprint=%s\n", dir, fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_realm_password_init(const char *password) {
|
||||||
|
if (!password || !password[0]) {
|
||||||
|
fprintf(stderr, "password required (or CST_REALM_PASSWORD)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX], fp[33];
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
||||||
|
fprintf(stderr, "cannot resolve realm directory\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (cst_realm_init_password(dir, password) != 0) {
|
||||||
|
fprintf(stderr, "password realm init failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_realm_keys_t k;
|
||||||
|
if (cst_realm_unlock_password(dir, password, &k) != 0)
|
||||||
|
return 1;
|
||||||
|
cst_realm_fingerprint(k.pk, fp);
|
||||||
|
printf("realm_password_mode dir=%s fingerprint=%s\n", dir, fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_login(const char *password, const char *instance_id,
|
||||||
|
const char *device_label) {
|
||||||
|
char err[256];
|
||||||
|
char *hand = cst_parliament_login(password, instance_id, device_label, err,
|
||||||
|
sizeof(err));
|
||||||
|
if (!hand) {
|
||||||
|
fprintf(stderr, "%s\n", err[0] ? err : "login failed");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fputs(hand, stdout);
|
||||||
|
free(hand);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_new_project(const char *project_id) {
|
||||||
|
if (cst_parliament_new_project(project_id) != 0) {
|
||||||
|
fprintf(stderr, "new project failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("parliament_new_project id=%s\n",
|
||||||
|
project_id && project_id[0] ? project_id : "default");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_realm_enroll(const char *instance_id, const char *label) {
|
||||||
|
if (!instance_id || !instance_id[0]) {
|
||||||
|
fprintf(stderr, "instance id required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
cst_realm_keys_t k;
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0 || cst_realm_load(dir, &k) != 0) {
|
||||||
|
fprintf(stderr, "load realm keys failed (run --realm-new first)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_realm_cert_t cert;
|
||||||
|
uint8_t seed[32], sk[64];
|
||||||
|
if (cst_realm_enroll(&k, instance_id, label, 0, &cert, seed, sk) != 0 ||
|
||||||
|
cst_realm_instance_save(instance_id, &cert, seed) != 0) {
|
||||||
|
fprintf(stderr, "enroll/save failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char fp[33];
|
||||||
|
cst_realm_fingerprint(k.pk, fp);
|
||||||
|
printf("enrolled id=%s label=%s realm=%s\n", instance_id,
|
||||||
|
label ? label : "", fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_peer_listen(int port, const char *instance_id) {
|
||||||
|
if (!instance_id || !instance_id[0]) {
|
||||||
|
fprintf(stderr, "instance id required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX], err[256];
|
||||||
|
cst_realm_keys_t realm;
|
||||||
|
cst_realm_cert_t cert;
|
||||||
|
uint8_t sk[64];
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0 || cst_realm_load(dir, &realm) != 0 ||
|
||||||
|
cst_realm_instance_load(instance_id, &cert, sk) != 0) {
|
||||||
|
fprintf(stderr, "cannot load realm/instance %s\n", instance_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (cst_realm_revoked(dir, instance_id)) {
|
||||||
|
fprintf(stderr, "instance revoked\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
os_net_init();
|
||||||
|
os_socket_t ls = os_net_listen("0.0.0.0", port > 0 ? port : 0, err, sizeof(err));
|
||||||
|
if (ls == OS_SOCKET_BAD) {
|
||||||
|
fprintf(stderr, "listen failed: %s\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int bound = os_net_port(ls);
|
||||||
|
printf("peer_listen port=%d id=%s\n", bound, instance_id);
|
||||||
|
fflush(stdout);
|
||||||
|
cst_peer_session_t sess;
|
||||||
|
if (cst_peer_accept((intptr_t)ls, &cert, sk, realm.pk, 60000, &sess, err,
|
||||||
|
sizeof(err)) != 0) {
|
||||||
|
fprintf(stderr, "accept/handshake failed: %s\n", err);
|
||||||
|
os_net_close(ls);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("peer_ok remote=%s host=%s\n", sess.remote.instance_id, sess.host);
|
||||||
|
os_net_close(ls);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_peer_dial(const char *host, int port,
|
||||||
|
const char *instance_id) {
|
||||||
|
if (!host || port <= 0 || !instance_id) {
|
||||||
|
fprintf(stderr, "host, port and instance id required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX], err[256];
|
||||||
|
cst_realm_keys_t realm;
|
||||||
|
cst_realm_cert_t cert;
|
||||||
|
uint8_t sk[64];
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0 || cst_realm_load(dir, &realm) != 0 ||
|
||||||
|
cst_realm_instance_load(instance_id, &cert, sk) != 0) {
|
||||||
|
fprintf(stderr, "cannot load realm/instance %s\n", instance_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_peer_session_t sess;
|
||||||
|
if (cst_peer_dial(host, port, &cert, sk, realm.pk, 30000, &sess, err,
|
||||||
|
sizeof(err)) != 0) {
|
||||||
|
fprintf(stderr, "dial/handshake failed: %s\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("peer_ok remote=%s\n", sess.remote.instance_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_parliament_cli_discover(const char *instance_id, int peer_port,
|
||||||
|
int wait_ms) {
|
||||||
|
if (!instance_id || peer_port <= 0) {
|
||||||
|
fprintf(stderr, "instance id and peer port required\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX], fp[33];
|
||||||
|
cst_realm_keys_t realm;
|
||||||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0 || cst_realm_load(dir, &realm) != 0 ||
|
||||||
|
cst_realm_fingerprint(realm.pk, fp) != 0) {
|
||||||
|
fprintf(stderr, "cannot load realm\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cst_discover_t *d = cst_discover_start(fp, instance_id, instance_id, peer_port);
|
||||||
|
if (!d) {
|
||||||
|
fprintf(stderr, "discover start failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int left = wait_ms > 0 ? wait_ms : 3000;
|
||||||
|
while (left > 0) {
|
||||||
|
cst_peer_info_t peers[16];
|
||||||
|
int n = cst_discover_peers(d, peers, 16);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
printf("peer iid=%s host=%s port=%d name=%s\n", peers[i].instance_id,
|
||||||
|
peers[i].host, peers[i].port, peers[i].name);
|
||||||
|
}
|
||||||
|
if (n > 0)
|
||||||
|
break;
|
||||||
|
sleep_ms(200);
|
||||||
|
left -= 200;
|
||||||
|
}
|
||||||
|
cst_discover_stop(d);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+173
@@ -213,6 +213,101 @@ int cst_realm_fingerprint(const uint8_t pk[32], char out[33]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int cst_realm_derive_password(cst_realm_keys_t *out, const char *password,
|
||||||
|
const uint8_t salt[16]) {
|
||||||
|
if (!out || !password || !password[0] || !salt)
|
||||||
|
return -1;
|
||||||
|
crypto_argon2_config cfg = {
|
||||||
|
.algorithm = CRYPTO_ARGON2_ID,
|
||||||
|
.nb_blocks = 8192, /* 8 MiB */
|
||||||
|
.nb_passes = 3,
|
||||||
|
.nb_lanes = 1,
|
||||||
|
};
|
||||||
|
crypto_argon2_inputs in = {
|
||||||
|
.pass = (const uint8_t *)password,
|
||||||
|
.salt = salt,
|
||||||
|
.pass_size = (uint32_t)strlen(password),
|
||||||
|
.salt_size = 16,
|
||||||
|
};
|
||||||
|
void *work = malloc((size_t)cfg.nb_blocks * 1024);
|
||||||
|
if (!work)
|
||||||
|
return -1;
|
||||||
|
uint8_t seed[32];
|
||||||
|
crypto_argon2(seed, 32, work, cfg, in, crypto_argon2_no_extras);
|
||||||
|
free(work);
|
||||||
|
int rc = cst_realm_derive(out, seed);
|
||||||
|
crypto_wipe(seed, sizeof(seed));
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_realm_is_password_mode(const char *dir) {
|
||||||
|
if (!dir)
|
||||||
|
return 0;
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0)
|
||||||
|
return 0;
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f)
|
||||||
|
return 0;
|
||||||
|
fclose(f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_realm_init_password(const char *dir, const char *password) {
|
||||||
|
if (!dir || !password || !password[0])
|
||||||
|
return -1;
|
||||||
|
if (os_mkdir_p(dir, 0700) != 0)
|
||||||
|
return -1;
|
||||||
|
uint8_t salt[16];
|
||||||
|
if (fill_random(salt, sizeof(salt)) != 0)
|
||||||
|
return -1;
|
||||||
|
cst_realm_keys_t k;
|
||||||
|
if (cst_realm_derive_password(&k, password, salt) != 0)
|
||||||
|
return -1;
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0)
|
||||||
|
return -1;
|
||||||
|
if (write_hex_file(path, salt, 16) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "public.key") != 0)
|
||||||
|
return -1;
|
||||||
|
if (write_hex_file(path, k.pk, 32) != 0)
|
||||||
|
return -1;
|
||||||
|
/* Never persist the seed/private key in password mode. */
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "mode") != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f)
|
||||||
|
return -1;
|
||||||
|
fputs("password\n", f);
|
||||||
|
fclose(f);
|
||||||
|
crypto_wipe(k.seed, sizeof(k.seed));
|
||||||
|
crypto_wipe(k.sk, sizeof(k.sk));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_realm_unlock_password(const char *dir, const char *password,
|
||||||
|
cst_realm_keys_t *out) {
|
||||||
|
if (!dir || !password || !out)
|
||||||
|
return -1;
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
uint8_t salt[16], pk_file[32];
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0 ||
|
||||||
|
read_hex_file(path, salt, 16) != 0)
|
||||||
|
return -1;
|
||||||
|
if (cst_realm_derive_password(out, password, salt) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "public.key") != 0 ||
|
||||||
|
read_hex_file(path, pk_file, 32) != 0 ||
|
||||||
|
memcmp(pk_file, out->pk, 32) != 0) {
|
||||||
|
crypto_wipe(out->seed, sizeof(out->seed));
|
||||||
|
crypto_wipe(out->sk, sizeof(out->sk));
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
return -1; /* wrong password */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int cst_realm_enroll(const cst_realm_keys_t *realm, const char *instance_id,
|
int cst_realm_enroll(const cst_realm_keys_t *realm, const char *instance_id,
|
||||||
const char *label, int64_t not_after, cst_realm_cert_t *cert,
|
const char *label, int64_t not_after, cst_realm_cert_t *cert,
|
||||||
uint8_t instance_seed[32], uint8_t instance_sk[64]) {
|
uint8_t instance_seed[32], uint8_t instance_sk[64]) {
|
||||||
@@ -367,3 +462,81 @@ int cst_realm_revoked(const char *dir, const char *instance_id) {
|
|||||||
fclose(f);
|
fclose(f);
|
||||||
return hit;
|
return hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int instance_dir(const char *instance_id, char *out, size_t n) {
|
||||||
|
char realm[OS_PATH_MAX], base[OS_PATH_MAX];
|
||||||
|
if (cst_realm_dir(realm, sizeof(realm)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(base, sizeof(base), realm, "instances") != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_mkdir_p(base, 0700) != 0)
|
||||||
|
return -1;
|
||||||
|
return os_path_join(out, n, base, instance_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_realm_instance_save(const char *instance_id, const cst_realm_cert_t *cert,
|
||||||
|
const uint8_t instance_seed[32]) {
|
||||||
|
if (!instance_id || !cert || !instance_seed)
|
||||||
|
return -1;
|
||||||
|
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||||
|
if (instance_dir(instance_id, dir, sizeof(dir)) != 0 ||
|
||||||
|
os_mkdir_p(dir, 0700) != 0)
|
||||||
|
return -1;
|
||||||
|
char *json = cst_realm_cert_to_json(cert);
|
||||||
|
if (!json)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "cert.json") != 0) {
|
||||||
|
free(json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
int ok = 0;
|
||||||
|
if (f) {
|
||||||
|
ok = fputs(json, f) >= 0 && fputc('\n', f) >= 0;
|
||||||
|
if (fclose(f) != 0)
|
||||||
|
ok = 0;
|
||||||
|
}
|
||||||
|
free(json);
|
||||||
|
if (!ok)
|
||||||
|
return -1;
|
||||||
|
(void)os_chmod_private(path);
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "seed") != 0)
|
||||||
|
return -1;
|
||||||
|
return write_hex_file(path, instance_seed, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_realm_instance_load(const char *instance_id, cst_realm_cert_t *cert,
|
||||||
|
uint8_t instance_sk[64]) {
|
||||||
|
if (!instance_id || !cert || !instance_sk)
|
||||||
|
return -1;
|
||||||
|
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||||
|
if (instance_dir(instance_id, dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "cert.json") != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f)
|
||||||
|
return -1;
|
||||||
|
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);
|
||||||
|
int ok = b.buf && cst_realm_cert_from_json(b.buf, cert) == 0;
|
||||||
|
cst_buf_free(&b);
|
||||||
|
if (!ok)
|
||||||
|
return -1;
|
||||||
|
uint8_t seed[32];
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "seed") != 0 ||
|
||||||
|
read_hex_file(path, seed, 32) != 0)
|
||||||
|
return -1;
|
||||||
|
uint8_t seed_copy[32], pk[32];
|
||||||
|
memcpy(seed_copy, seed, 32);
|
||||||
|
crypto_ed25519_key_pair(instance_sk, pk, seed_copy);
|
||||||
|
crypto_wipe(seed_copy, sizeof(seed_copy));
|
||||||
|
crypto_wipe(seed, sizeof(seed));
|
||||||
|
if (memcmp(pk, cert->instance_pk, 32) != 0)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ int cst_settings_load(cst_settings_t *s) {
|
|||||||
cJSON_GetObjectItemCaseSensitive(root, "mode"));
|
cJSON_GetObjectItemCaseSensitive(root, "mode"));
|
||||||
copy_str(s->approval, sizeof(s->approval),
|
copy_str(s->approval, sizeof(s->approval),
|
||||||
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
||||||
|
copy_str(s->collaboration, sizeof(s->collaboration),
|
||||||
|
cJSON_GetObjectItemCaseSensitive(root, "collaboration"));
|
||||||
s->show_thinking =
|
s->show_thinking =
|
||||||
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root, "show_thinking"));
|
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root, "show_thinking"));
|
||||||
const cJSON *tr = cJSON_GetObjectItemCaseSensitive(root, "trusted_workspaces");
|
const cJSON *tr = cJSON_GetObjectItemCaseSensitive(root, "trusted_workspaces");
|
||||||
@@ -111,6 +113,8 @@ int cst_settings_save(const cst_settings_t *s) {
|
|||||||
cJSON_AddStringToObject(root, "mode", s->mode);
|
cJSON_AddStringToObject(root, "mode", s->mode);
|
||||||
if (s->approval[0])
|
if (s->approval[0])
|
||||||
cJSON_AddStringToObject(root, "approval", s->approval);
|
cJSON_AddStringToObject(root, "approval", s->approval);
|
||||||
|
if (s->collaboration[0])
|
||||||
|
cJSON_AddStringToObject(root, "collaboration", s->collaboration);
|
||||||
if (s->show_thinking)
|
if (s->show_thinking)
|
||||||
cJSON_AddTrueToObject(root, "show_thinking");
|
cJSON_AddTrueToObject(root, "show_thinking");
|
||||||
if (s->ntrusted) {
|
if (s->ntrusted) {
|
||||||
|
|||||||
@@ -775,6 +775,14 @@ static int task_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
|||||||
(void)ud;
|
(void)ud;
|
||||||
if (!ctx->agent)
|
if (!ctx->agent)
|
||||||
return out_text(r, 1, "error: task needs a running agent");
|
return out_text(r, 1, "error: task needs a running agent");
|
||||||
|
{
|
||||||
|
cst_settings_t s; cst_settings_load(&s);
|
||||||
|
const char *mode = getenv("CST_COLLABORATION");
|
||||||
|
if ((!mode || !mode[0]) && s.collaboration[0]) mode = s.collaboration;
|
||||||
|
if (mode && !strcmp(mode, "parliament"))
|
||||||
|
return out_text(r, 1, "error: collaboration=parliament replaces in-process "
|
||||||
|
"workers; use Realm peer seats");
|
||||||
|
}
|
||||||
const cst_agent_config_t *pcfg = cst_agent_config(ctx->agent);
|
const cst_agent_config_t *pcfg = cst_agent_config(ctx->agent);
|
||||||
if (pcfg->depth > 0)
|
if (pcfg->depth > 0)
|
||||||
return out_text(r, 1, "error: a sub-agent cannot start sub-agents; do "
|
return out_text(r, 1, "error: a sub-agent cannot start sub-agents; do "
|
||||||
@@ -1179,6 +1187,13 @@ static int async_worker_tool(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c
|
|||||||
if (!ctx->agent) return out_text(out, 1, "error: no agent");
|
if (!ctx->agent) return out_text(out, 1, "error: no agent");
|
||||||
const cst_agent_config_t *cfg = cst_agent_config(ctx->agent);
|
const cst_agent_config_t *cfg = cst_agent_config(ctx->agent);
|
||||||
if (cfg->depth || !cfg->runtime) return out_text(out, 1, "error: only the main agent can schedule workers");
|
if (cfg->depth || !cfg->runtime) return out_text(out, 1, "error: only the main agent can schedule workers");
|
||||||
|
{
|
||||||
|
cst_settings_t s; cst_settings_load(&s);
|
||||||
|
const char *mode = getenv("CST_COLLABORATION");
|
||||||
|
if ((!mode || !mode[0]) && s.collaboration[0]) mode = s.collaboration;
|
||||||
|
if (mode && !strcmp(mode, "parliament"))
|
||||||
|
return out_text(out, 1, "error: collaboration=parliament replaces in-process workers; use Realm peer seats");
|
||||||
|
}
|
||||||
if (strcmp(c->name, "worker_start")) {
|
if (strcmp(c->name, "worker_start")) {
|
||||||
const char *wt = arg(c, "worktree"); int code;
|
const char *wt = arg(c, "worktree"); int code;
|
||||||
if (wt && cst_runtime_resource_busy(cfg->runtime, wt)) return out_text(out, 1, "error: worker is still using this worktree; wait before review/integration");
|
if (wt && cst_runtime_resource_busy(cfg->runtime, wt)) return out_text(out, 1, "error: worker is still using this worktree; wait before review/integration");
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
#include "cst_parliament.h"
|
||||||
|
#include "cst_realm.h"
|
||||||
|
#include "cst_test_util.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
char dir[2048], cfg[512];
|
||||||
|
assert(!test_tmpdir(dir, sizeof(dir), "parliament"));
|
||||||
|
assert(snprintf(cfg, sizeof(cfg), "%s/cfg", dir) < (int)sizeof(cfg));
|
||||||
|
assert(!os_mkdir_p(cfg, 0700));
|
||||||
|
setenv("XDG_CONFIG_HOME", cfg, 1);
|
||||||
|
|
||||||
|
char realm[OS_PATH_MAX];
|
||||||
|
assert(cst_realm_dir(realm, sizeof(realm)) == 0);
|
||||||
|
assert(cst_realm_init_password(realm, "correct horse battery") == 0);
|
||||||
|
assert(cst_realm_is_password_mode(realm));
|
||||||
|
|
||||||
|
cst_realm_keys_t bad, good;
|
||||||
|
assert(cst_realm_unlock_password(realm, "wrong", &bad) != 0);
|
||||||
|
assert(cst_realm_unlock_password(realm, "correct horse battery", &good) == 0);
|
||||||
|
|
||||||
|
char err[128];
|
||||||
|
char *hand = cst_parliament_login("correct horse battery", "phone-1", "pixel",
|
||||||
|
err, sizeof(err));
|
||||||
|
assert(hand && strstr(hand, "LOGIN_OK") && strstr(hand, "chair=phone-1"));
|
||||||
|
free(hand);
|
||||||
|
|
||||||
|
cst_parliament_meta_t m;
|
||||||
|
assert(cst_parliament_load(&m) == 0);
|
||||||
|
assert(!strcmp(m.chair_instance, "phone-1"));
|
||||||
|
|
||||||
|
assert(cst_parliament_context_put("shared agenda v1") == 0);
|
||||||
|
hand = cst_parliament_login("correct horse battery", "laptop-2", "thinkpad",
|
||||||
|
err, sizeof(err));
|
||||||
|
assert(hand && strstr(hand, "handover_from=phone-1"));
|
||||||
|
assert(strstr(hand, "shared agenda v1"));
|
||||||
|
free(hand);
|
||||||
|
assert(cst_parliament_load(&m) == 0);
|
||||||
|
assert(!strcmp(m.chair_instance, "laptop-2"));
|
||||||
|
|
||||||
|
assert(cst_parliament_new_project("proj-b") == 0);
|
||||||
|
char *ctx = cst_parliament_context_get();
|
||||||
|
assert(ctx && ctx[0] == 0);
|
||||||
|
free(ctx);
|
||||||
|
assert(cst_parliament_load(&m) == 0);
|
||||||
|
assert(!strcmp(m.project_id, "proj-b"));
|
||||||
|
assert(m.chair_instance[0] == 0);
|
||||||
|
|
||||||
|
test_rmrf(dir);
|
||||||
|
puts("test_parliament: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user