@cello-protocol/daemon 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +410 -186
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +37 -9
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +186 -54
- package/dist/db-identity-store.js.map +1 -1
- package/dist/http-manifest-poll.d.ts +67 -0
- package/dist/http-manifest-poll.d.ts.map +1 -0
- package/dist/http-manifest-poll.js +143 -0
- package/dist/http-manifest-poll.js.map +1 -0
- package/dist/identity-migration.js +14 -8
- package/dist/identity-migration.js.map +1 -1
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +1 -0
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +11 -2
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-identity-store.d.ts","sourceRoot":"","sources":["../src/db-identity-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"db-identity-store.d.ts","sourceRoot":"","sources":["../src/db-identity-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,49 +16,126 @@
|
|
|
16
16
|
* The K_local seed and FROST/ML-DSA secrets are stored as BLOB columns inside the encrypted DB; they
|
|
17
17
|
* are NEVER written to a flat file and NEVER logged (SI-001). The row is the only home.
|
|
18
18
|
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
19
20
|
const ML_DSA_ALGORITHM = "ML-DSA-44";
|
|
21
|
+
/**
|
|
22
|
+
* The `agents` schema (CELLO-M7-REMOVE-001 DOD-REMOVE-1). The store is keyed by a STABLE `agent_id`,
|
|
23
|
+
* not by `agent_name` — guardrail #1: durable identity hangs off agent_id; the human name and the
|
|
24
|
+
* pubkey are mutable ATTRIBUTES. A partial unique index makes `agent_name` unique only among
|
|
25
|
+
* NON-retired rows, so removal (state='retired') frees the name for reuse while the retired identity
|
|
26
|
+
* survives for accountability (SI-002). `state`: 'created' | 'registered' | 'retired'.
|
|
27
|
+
*/
|
|
28
|
+
const CREATE_AGENTS_SQL = `
|
|
29
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
30
|
+
agent_id TEXT PRIMARY KEY,
|
|
31
|
+
agent_name TEXT NOT NULL,
|
|
32
|
+
-- K_local Ed25519 identity (PERSIST-002: seed moves from agents/<name>/key into this BLOB).
|
|
33
|
+
k_local_seed BLOB NOT NULL,
|
|
34
|
+
k_local_pubkey TEXT NOT NULL,
|
|
35
|
+
-- lifecycle: 'created' (K_local exists, not yet registered) | 'registered' | 'retired' (REMOVE-001).
|
|
36
|
+
state TEXT NOT NULL DEFAULT 'created',
|
|
37
|
+
-- ML-DSA keypair (was ml-dsa-keypair.json).
|
|
38
|
+
ml_dsa_pubkey TEXT,
|
|
39
|
+
ml_dsa_secret BLOB,
|
|
40
|
+
ml_dsa_algorithm TEXT,
|
|
41
|
+
-- FROST signing share (was frost-share.json).
|
|
42
|
+
frost_epoch_id TEXT,
|
|
43
|
+
frost_primary_pubkey TEXT,
|
|
44
|
+
frost_identifier TEXT,
|
|
45
|
+
frost_signing_share BLOB,
|
|
46
|
+
frost_threshold INTEGER,
|
|
47
|
+
frost_participants INTEGER,
|
|
48
|
+
frost_commitments BLOB,
|
|
49
|
+
frost_verifying_shares BLOB,
|
|
50
|
+
frost_dkg_method TEXT,
|
|
51
|
+
-- registration record (was registration-state.json).
|
|
52
|
+
reg_agent_id TEXT,
|
|
53
|
+
reg_primary_pubkey TEXT,
|
|
54
|
+
reg_ml_dsa_pubkey TEXT,
|
|
55
|
+
reg_registered_at INTEGER,
|
|
56
|
+
reg_status TEXT,
|
|
57
|
+
-- agent↔user link captured at registration (was agent-user-link.json).
|
|
58
|
+
link_agent_id TEXT,
|
|
59
|
+
link_pre_auth_token TEXT,
|
|
60
|
+
link_linked_at INTEGER,
|
|
61
|
+
created_at INTEGER NOT NULL,
|
|
62
|
+
updated_at INTEGER NOT NULL
|
|
63
|
+
)
|
|
64
|
+
`;
|
|
65
|
+
// A name is unique only among NON-retired agents — the DB-level backstop for "free the name on removal".
|
|
66
|
+
const CREATE_ACTIVE_NAME_INDEX_SQL = "CREATE UNIQUE INDEX IF NOT EXISTS agents_active_name ON agents(agent_name) WHERE state != 'retired'";
|
|
67
|
+
// The columns of the PRE-REMOVE-001 `agents` table (agent_name PK, no agent_id), in order. Used to
|
|
68
|
+
// copy rows verbatim during the one-time re-key rebuild.
|
|
69
|
+
const PRE_REKEY_COLUMNS = [
|
|
70
|
+
"agent_name",
|
|
71
|
+
"k_local_seed",
|
|
72
|
+
"k_local_pubkey",
|
|
73
|
+
"state",
|
|
74
|
+
"ml_dsa_pubkey",
|
|
75
|
+
"ml_dsa_secret",
|
|
76
|
+
"ml_dsa_algorithm",
|
|
77
|
+
"frost_epoch_id",
|
|
78
|
+
"frost_primary_pubkey",
|
|
79
|
+
"frost_identifier",
|
|
80
|
+
"frost_signing_share",
|
|
81
|
+
"frost_threshold",
|
|
82
|
+
"frost_participants",
|
|
83
|
+
"frost_commitments",
|
|
84
|
+
"frost_verifying_shares",
|
|
85
|
+
"frost_dkg_method",
|
|
86
|
+
"reg_agent_id",
|
|
87
|
+
"reg_primary_pubkey",
|
|
88
|
+
"reg_ml_dsa_pubkey",
|
|
89
|
+
"reg_registered_at",
|
|
90
|
+
"reg_status",
|
|
91
|
+
"link_agent_id",
|
|
92
|
+
"link_pre_auth_token",
|
|
93
|
+
"link_linked_at",
|
|
94
|
+
"created_at",
|
|
95
|
+
"updated_at",
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* One-time re-key of a PRE-REMOVE-001 `agents` table (PRIMARY KEY agent_name, no agent_id) to the new
|
|
99
|
+
* stable-agent_id shape. Rebuilds the table, backfilling a fresh agent_id per existing row. Runs at
|
|
100
|
+
* most once — `ensureIdentitySchema` guards it behind a column-presence check — and is wrapped in a
|
|
101
|
+
* transaction so a crash mid-rebuild leaves the original table intact. Works on node:sqlite (in-memory
|
|
102
|
+
* test handles) and SQLCipher alike.
|
|
103
|
+
*/
|
|
104
|
+
function rebuildAgentsToAgentIdPk(db) {
|
|
105
|
+
const cols = PRE_REKEY_COLUMNS.join(", ");
|
|
106
|
+
db.exec("BEGIN");
|
|
107
|
+
try {
|
|
108
|
+
db.exec("ALTER TABLE agents RENAME TO agents_pre_rekey");
|
|
109
|
+
db.exec(CREATE_AGENTS_SQL);
|
|
110
|
+
db.exec(`INSERT INTO agents (agent_id, ${cols})
|
|
111
|
+
SELECT lower(hex(randomblob(16))), ${cols} FROM agents_pre_rekey`);
|
|
112
|
+
db.exec("DROP TABLE agents_pre_rekey");
|
|
113
|
+
db.exec("COMMIT");
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
try {
|
|
117
|
+
db.exec("ROLLBACK");
|
|
118
|
+
}
|
|
119
|
+
catch { /* the failing statement may have already aborted the txn */ }
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
20
123
|
/**
|
|
21
124
|
* Idempotent schema for the identity store. Called once at daemon init AND defensively by each
|
|
22
|
-
* DbRegistrationPersistence constructor
|
|
23
|
-
*
|
|
125
|
+
* DbRegistrationPersistence constructor, so the store works whether or not the composition root has
|
|
126
|
+
* run its own ensure. Creates the table on a fresh DB, re-keys a pre-REMOVE-001 table to the
|
|
127
|
+
* stable-agent_id shape once, and (always) ensures the active-name partial unique index.
|
|
24
128
|
*/
|
|
25
129
|
export function ensureIdentitySchema(db) {
|
|
26
|
-
db.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ml_dsa_pubkey TEXT,
|
|
36
|
-
ml_dsa_secret BLOB,
|
|
37
|
-
ml_dsa_algorithm TEXT,
|
|
38
|
-
-- FROST signing share (was frost-share.json).
|
|
39
|
-
frost_epoch_id TEXT,
|
|
40
|
-
frost_primary_pubkey TEXT,
|
|
41
|
-
frost_identifier TEXT,
|
|
42
|
-
frost_signing_share BLOB,
|
|
43
|
-
frost_threshold INTEGER,
|
|
44
|
-
frost_participants INTEGER,
|
|
45
|
-
frost_commitments BLOB,
|
|
46
|
-
frost_verifying_shares BLOB,
|
|
47
|
-
frost_dkg_method TEXT,
|
|
48
|
-
-- registration record (was registration-state.json).
|
|
49
|
-
reg_agent_id TEXT,
|
|
50
|
-
reg_primary_pubkey TEXT,
|
|
51
|
-
reg_ml_dsa_pubkey TEXT,
|
|
52
|
-
reg_registered_at INTEGER,
|
|
53
|
-
reg_status TEXT,
|
|
54
|
-
-- agent↔user link captured at registration (was agent-user-link.json).
|
|
55
|
-
link_agent_id TEXT,
|
|
56
|
-
link_pre_auth_token TEXT,
|
|
57
|
-
link_linked_at INTEGER,
|
|
58
|
-
created_at INTEGER NOT NULL,
|
|
59
|
-
updated_at INTEGER NOT NULL
|
|
60
|
-
)
|
|
61
|
-
`);
|
|
130
|
+
const cols = db.prepare("PRAGMA table_info(agents)").all();
|
|
131
|
+
if (cols.length === 0) {
|
|
132
|
+
db.exec(CREATE_AGENTS_SQL);
|
|
133
|
+
}
|
|
134
|
+
else if (!cols.some((c) => c.name === "agent_id")) {
|
|
135
|
+
// A pre-REMOVE-001 table exists (agent_name PK). Re-key it once to the stable-agent_id shape.
|
|
136
|
+
rebuildAgentsToAgentIdPk(db);
|
|
137
|
+
}
|
|
138
|
+
db.exec(CREATE_ACTIVE_NAME_INDEX_SQL);
|
|
62
139
|
}
|
|
63
140
|
const toBuf = (b) => Buffer.from(b);
|
|
64
141
|
const toBytes = (v) => v instanceof Uint8Array ? new Uint8Array(v) : Buffer.isBuffer(v) ? new Uint8Array(v) : new Uint8Array(0);
|
|
@@ -70,34 +147,85 @@ export class DbIdentityStore {
|
|
|
70
147
|
this.#logger = logger;
|
|
71
148
|
ensureIdentitySchema(db);
|
|
72
149
|
}
|
|
73
|
-
/** True if an agent row with this name
|
|
74
|
-
|
|
75
|
-
const row = this.#db
|
|
150
|
+
/** True if an ACTIVE (non-retired) agent row with this name exists — the create-collision check. */
|
|
151
|
+
hasActiveAgent(agentName) {
|
|
152
|
+
const row = this.#db
|
|
153
|
+
.prepare("SELECT 1 AS one FROM agents WHERE agent_name = ? AND state != 'retired'")
|
|
154
|
+
.get(agentName);
|
|
76
155
|
return row !== undefined;
|
|
77
156
|
}
|
|
78
157
|
/**
|
|
79
|
-
* Create a new agent row holding a fresh K_local seed (AC-004)
|
|
80
|
-
*
|
|
81
|
-
*
|
|
158
|
+
* Create a new agent row holding a fresh K_local seed (AC-004), keyed by a freshly-minted stable
|
|
159
|
+
* agent_id (REMOVE-001 DOD-REMOVE-1; guardrail #1). Explicit only — callers generate the seed
|
|
160
|
+
* (crypto) and pass it in. Throws if an ACTIVE agent already holds the name (no silent overwrite); a
|
|
161
|
+
* RETIRED row with the same name does NOT block — the name has been freed. Returns the new agent_id.
|
|
82
162
|
*/
|
|
83
163
|
createAgent(agentName, kLocalSeed, kLocalPubkeyHex) {
|
|
84
|
-
if (this.
|
|
164
|
+
if (this.hasActiveAgent(agentName)) {
|
|
85
165
|
throw new Error(`agent '${agentName}' already exists`);
|
|
86
166
|
}
|
|
167
|
+
const agentId = randomUUID();
|
|
87
168
|
const now = Date.now();
|
|
88
169
|
this.#db
|
|
89
|
-
.prepare(`INSERT INTO agents (agent_name, k_local_seed, k_local_pubkey, state, created_at, updated_at)
|
|
90
|
-
VALUES (?, ?, ?, 'created', ?, ?)`)
|
|
91
|
-
.run(agentName, toBuf(kLocalSeed), kLocalPubkeyHex, now, now);
|
|
92
|
-
// SI-001: the seed is NEVER logged — only the agent name and PUBLIC key.
|
|
93
|
-
this.#logger.info("persist.identity.created", { agentName, agentPubkey: kLocalPubkeyHex });
|
|
170
|
+
.prepare(`INSERT INTO agents (agent_id, agent_name, k_local_seed, k_local_pubkey, state, created_at, updated_at)
|
|
171
|
+
VALUES (?, ?, ?, ?, 'created', ?, ?)`)
|
|
172
|
+
.run(agentId, agentName, toBuf(kLocalSeed), kLocalPubkeyHex, now, now);
|
|
173
|
+
// SI-001: the seed is NEVER logged — only the agent name, the agent_id, and the PUBLIC key.
|
|
174
|
+
this.#logger.info("persist.identity.created", { agentName, agentId, agentPubkey: kLocalPubkeyHex });
|
|
175
|
+
return agentId;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Retire (REMOVE-001 DOD-REMOVE-1): flip the ACTIVE row for `agentName` to state='retired' WITHOUT
|
|
179
|
+
* deleting the row, its keys, or its history (SI-002 — accountability survives). One-way. The name
|
|
180
|
+
* is freed (the partial unique index excludes retired rows). Returns the retired agent_id, or null if
|
|
181
|
+
* there was no active agent with that name (fail-loud at the caller).
|
|
182
|
+
*/
|
|
183
|
+
retireAgent(agentName) {
|
|
184
|
+
const active = this.#db
|
|
185
|
+
.prepare("SELECT agent_id FROM agents WHERE agent_name = ? AND state != 'retired'")
|
|
186
|
+
.get(agentName);
|
|
187
|
+
if (!active)
|
|
188
|
+
return null;
|
|
189
|
+
const res = this.#db
|
|
190
|
+
.prepare("UPDATE agents SET state = 'retired', updated_at = ? WHERE agent_id = ?")
|
|
191
|
+
.run(Date.now(), active.agent_id);
|
|
192
|
+
if (Number(res.changes) === 0)
|
|
193
|
+
return null;
|
|
194
|
+
return active.agent_id;
|
|
94
195
|
}
|
|
95
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Read the row to act on for a removal / directory-revocation re-push (REMOVE-001 DOD-REMOVE-2): the
|
|
198
|
+
* ACTIVE row for the name if one exists (a fresh removal), else the MOST-RECENTLY-retired row (a
|
|
199
|
+
* re-push of an already-retired agent whose directory revocation did not land — DB-001). Includes the
|
|
200
|
+
* K_local seed (to re-sign the revocation) and the DIRECTORY-known reg_agent_id (what the directory is
|
|
201
|
+
* asked to revoke; null if the agent was never registered). Returns null if no row with this name
|
|
202
|
+
* exists at all.
|
|
203
|
+
*/
|
|
204
|
+
getAgentForRevocation(agentName) {
|
|
205
|
+
const r = this.#db
|
|
206
|
+
.prepare(`SELECT agent_id, reg_agent_id, k_local_seed, state FROM agents WHERE agent_name = ?
|
|
207
|
+
ORDER BY (state != 'retired') DESC, updated_at DESC LIMIT 1`)
|
|
208
|
+
.get(agentName);
|
|
209
|
+
if (!r)
|
|
210
|
+
return null;
|
|
211
|
+
return {
|
|
212
|
+
localAgentId: r.agent_id,
|
|
213
|
+
regAgentId: r.reg_agent_id ?? null,
|
|
214
|
+
kLocalSeed: toBytes(r.k_local_seed),
|
|
215
|
+
state: r.state,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Enumerate ACTIVE agents (agent_id + name + seed + pubkey + state) for the daemon's startup loader.
|
|
220
|
+
* Retired rows are EXCLUDED — they must never be resurrected into the runtime — but remain in the DB
|
|
221
|
+
* and are readable directly for accountability (SI-002).
|
|
222
|
+
*/
|
|
96
223
|
listAgents() {
|
|
97
224
|
const rows = this.#db
|
|
98
|
-
.prepare("SELECT agent_name, k_local_seed, k_local_pubkey, state FROM agents ORDER BY agent_name ASC")
|
|
225
|
+
.prepare("SELECT agent_id, agent_name, k_local_seed, k_local_pubkey, state FROM agents WHERE state != 'retired' ORDER BY agent_name ASC")
|
|
99
226
|
.all();
|
|
100
227
|
return rows.map((r) => ({
|
|
228
|
+
agentId: r.agent_id,
|
|
101
229
|
agentName: r.agent_name,
|
|
102
230
|
kLocalSeed: toBytes(r.k_local_seed),
|
|
103
231
|
kLocalPubkey: r.k_local_pubkey,
|
|
@@ -123,8 +251,11 @@ export class DbRegistrationPersistence {
|
|
|
123
251
|
}
|
|
124
252
|
#updateRow(setClause, params) {
|
|
125
253
|
const now = Date.now();
|
|
254
|
+
// REMOVE-001: target the ACTIVE row for this name. After name reuse a retired row and a new active
|
|
255
|
+
// row can share a name; the partial unique index guarantees at most one active row, so this is
|
|
256
|
+
// unambiguous. A retired identity's registration material is never mutated.
|
|
126
257
|
const res = this.#db
|
|
127
|
-
.prepare(`UPDATE agents SET ${setClause}, updated_at = ? WHERE agent_name =
|
|
258
|
+
.prepare(`UPDATE agents SET ${setClause}, updated_at = ? WHERE agent_name = ? AND state != 'retired'`)
|
|
128
259
|
.run(...params, now, this.#agentName);
|
|
129
260
|
if (Number(res.changes) === 0) {
|
|
130
261
|
// The agent row must exist (created by the agent-creation path before registration). A missing
|
|
@@ -181,7 +312,8 @@ export class DbRegistrationPersistence {
|
|
|
181
312
|
}
|
|
182
313
|
// ─── Load (restart rehydration) ──────────────────────────────────────────────
|
|
183
314
|
#row() {
|
|
184
|
-
|
|
315
|
+
// REMOVE-001: load the ACTIVE row for this name (never a retired tombstone).
|
|
316
|
+
return this.#db.prepare("SELECT * FROM agents WHERE agent_name = ? AND state != 'retired'").get(this.#agentName);
|
|
185
317
|
}
|
|
186
318
|
async loadRegistrationState() {
|
|
187
319
|
const r = this.#row();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-identity-store.js","sourceRoot":"","sources":["../src/db-identity-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"db-identity-store.js","sourceRoot":"","sources":["../src/db-identity-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO-M7-CONN-001 (DOD-CONN-3) — HTTP consortium-manifest poll.
|
|
3
|
+
*
|
|
4
|
+
* The manifest poll is the ONE daemon-level directory operation. With the keystone
|
|
5
|
+
* deleted there is no shared authenticated signaling stream to carry it, so it moves
|
|
6
|
+
* to unauthenticated HTTP: GET ${directoryUrl}/manifest. The manifest is PUBLIC,
|
|
7
|
+
* self-authenticating data — threshold-signed by officer root keys that the daemon has
|
|
8
|
+
* pinned LOCALLY (CELLO_CONSORTIUM_ROOT_KEYS). The directory is ONLY a transport, never
|
|
9
|
+
* a trust anchor: a rogue/compromised directory cannot make us adopt a forged, expired,
|
|
10
|
+
* not-yet-valid, or rolled-back manifest. The verify-before-adopt policy here is the
|
|
11
|
+
* SAME TUF policy the deleted keystone signaling path enforced (SignalingManager
|
|
12
|
+
* .handleManifestPollResponse), preserved unchanged across the transport move.
|
|
13
|
+
*
|
|
14
|
+
* Runs daemon-level — even with ZERO agents (the property the keystone, which borrowed
|
|
15
|
+
* the primary agent's identity to authenticate, could never provide).
|
|
16
|
+
*
|
|
17
|
+
* Crypto reference: RFC 8032 (Ed25519 threshold verification via verifyManifest).
|
|
18
|
+
*/
|
|
19
|
+
import type { IManifestProvider, IManifestVersionStore, IManifestPollScheduler } from "@cello-protocol/transport";
|
|
20
|
+
/** Minimal structural logger — events use the `domain.noun.verb` taxonomy. */
|
|
21
|
+
export interface ManifestPollLogger {
|
|
22
|
+
info(event: string, ctx?: Record<string, unknown>): void;
|
|
23
|
+
warn(event: string, ctx?: Record<string, unknown>): void;
|
|
24
|
+
error(event: string, ctx?: Record<string, unknown>): void;
|
|
25
|
+
}
|
|
26
|
+
export interface ManifestAdoptDeps {
|
|
27
|
+
manifestProvider: IManifestProvider;
|
|
28
|
+
manifestVersionStore: IManifestVersionStore;
|
|
29
|
+
/** Locally-pinned officer root keys (CELLO_CONSORTIUM_ROOT_KEYS). */
|
|
30
|
+
rootKeys: readonly string[];
|
|
31
|
+
threshold: number;
|
|
32
|
+
logger: ManifestPollLogger;
|
|
33
|
+
}
|
|
34
|
+
export type ManifestPollFailureReason = "manifest_http_unreachable" | "manifest_malformed" | "manifest_threshold_invalid" | "manifest_signature_invalid" | "manifest_not_yet_valid" | "manifest_expired" | "manifest_version_rollback" | "manifest_store_error";
|
|
35
|
+
export type ManifestPollOutcome = {
|
|
36
|
+
ok: true;
|
|
37
|
+
adopted: boolean;
|
|
38
|
+
oldVersion: number | null;
|
|
39
|
+
newVersion: number;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
reason: ManifestPollFailureReason;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Fetch the consortium manifest over HTTP, verify it (TUF), and adopt it if it is a
|
|
46
|
+
* newer, validly-signed, unexpired manifest. Never throws — a poll failure returns a
|
|
47
|
+
* distinct reason and leaves the cached manifest + trusted version untouched (DB-001).
|
|
48
|
+
*/
|
|
49
|
+
export declare function pollManifestOverHttp(opts: {
|
|
50
|
+
directoryUrl: string;
|
|
51
|
+
fetchFn?: typeof fetch;
|
|
52
|
+
now?: Date;
|
|
53
|
+
correlationId?: string;
|
|
54
|
+
} & ManifestAdoptDeps): Promise<ManifestPollOutcome>;
|
|
55
|
+
/**
|
|
56
|
+
* Wire the daemon-level HTTP manifest poll onto a scheduler. Self-rearming: each tick
|
|
57
|
+
* polls then schedules the next. Returns a stop function. The poll runs regardless of
|
|
58
|
+
* how many agents exist (including zero) — it has no agent identity and opens no
|
|
59
|
+
* signaling connection.
|
|
60
|
+
*/
|
|
61
|
+
export declare function startHttpManifestPoll(opts: {
|
|
62
|
+
scheduler: IManifestPollScheduler;
|
|
63
|
+
directoryUrl: string;
|
|
64
|
+
fetchFn?: typeof fetch;
|
|
65
|
+
mintCorrelationId?: () => string;
|
|
66
|
+
} & ManifestAdoptDeps): () => void;
|
|
67
|
+
//# sourceMappingURL=http-manifest-poll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-manifest-poll.d.ts","sourceRoot":"","sources":["../src/http-manifest-poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAElH,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC3D;AAED,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,oBAAoB,EAAE,qBAAqB,CAAC;IAC5C,qEAAqE;IACrE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,MAAM,yBAAyB,GACjC,2BAA2B,GAC3B,oBAAoB,GACpB,4BAA4B,GAC5B,4BAA4B,GAC5B,wBAAwB,GACxB,kBAAkB,GAClB,2BAA2B,GAC3B,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAIrD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE;IACJ,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,iBAAiB,GACpB,OAAO,CAAC,mBAAmB,CAAC,CA+F9B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE;IACJ,SAAS,EAAE,sBAAsB,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAC;CAClC,GAAG,iBAAiB,GACpB,MAAM,IAAI,CA6BZ"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO-M7-CONN-001 (DOD-CONN-3) — HTTP consortium-manifest poll.
|
|
3
|
+
*
|
|
4
|
+
* The manifest poll is the ONE daemon-level directory operation. With the keystone
|
|
5
|
+
* deleted there is no shared authenticated signaling stream to carry it, so it moves
|
|
6
|
+
* to unauthenticated HTTP: GET ${directoryUrl}/manifest. The manifest is PUBLIC,
|
|
7
|
+
* self-authenticating data — threshold-signed by officer root keys that the daemon has
|
|
8
|
+
* pinned LOCALLY (CELLO_CONSORTIUM_ROOT_KEYS). The directory is ONLY a transport, never
|
|
9
|
+
* a trust anchor: a rogue/compromised directory cannot make us adopt a forged, expired,
|
|
10
|
+
* not-yet-valid, or rolled-back manifest. The verify-before-adopt policy here is the
|
|
11
|
+
* SAME TUF policy the deleted keystone signaling path enforced (SignalingManager
|
|
12
|
+
* .handleManifestPollResponse), preserved unchanged across the transport move.
|
|
13
|
+
*
|
|
14
|
+
* Runs daemon-level — even with ZERO agents (the property the keystone, which borrowed
|
|
15
|
+
* the primary agent's identity to authenticate, could never provide).
|
|
16
|
+
*
|
|
17
|
+
* Crypto reference: RFC 8032 (Ed25519 threshold verification via verifyManifest).
|
|
18
|
+
*/
|
|
19
|
+
import { verifyManifest } from "@cello-protocol/crypto";
|
|
20
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch the consortium manifest over HTTP, verify it (TUF), and adopt it if it is a
|
|
23
|
+
* newer, validly-signed, unexpired manifest. Never throws — a poll failure returns a
|
|
24
|
+
* distinct reason and leaves the cached manifest + trusted version untouched (DB-001).
|
|
25
|
+
*/
|
|
26
|
+
export async function pollManifestOverHttp(opts) {
|
|
27
|
+
const { directoryUrl, fetchFn = fetch, now = new Date(), correlationId, manifestProvider, manifestVersionStore, rootKeys, threshold, logger, } = opts;
|
|
28
|
+
const manifestUrl = `${directoryUrl.replace(/\/$/, "")}/manifest`;
|
|
29
|
+
// 1. Fetch. Any network error / non-200 / timeout → unreachable. DB-001: the caller
|
|
30
|
+
// keeps the cached manifest; a poll failure never disturbs agent connections.
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
const ac = new AbortController();
|
|
34
|
+
const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
35
|
+
let resp;
|
|
36
|
+
try {
|
|
37
|
+
resp = await fetchFn(manifestUrl, { signal: ac.signal });
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
if (!resp.ok) {
|
|
43
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_http_unreachable", status: resp.status, directoryUrl, correlationId });
|
|
44
|
+
return { ok: false, reason: "manifest_http_unreachable" };
|
|
45
|
+
}
|
|
46
|
+
raw = await resp.json();
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_http_unreachable", directoryUrl, correlationId, error: err instanceof Error ? err.message : String(err) });
|
|
50
|
+
return { ok: false, reason: "manifest_http_unreachable" };
|
|
51
|
+
}
|
|
52
|
+
// 2. Structural check before verify — a junk body must fail loudly, not be cached.
|
|
53
|
+
const manifest = raw;
|
|
54
|
+
if (typeof manifest?.version !== "number" ||
|
|
55
|
+
!Array.isArray(manifest?.nodes) ||
|
|
56
|
+
!Array.isArray(manifest?.signatures)) {
|
|
57
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_malformed", directoryUrl, correlationId });
|
|
58
|
+
return { ok: false, reason: "manifest_malformed" };
|
|
59
|
+
}
|
|
60
|
+
// 3. Defense-in-depth: never adopt against a 0/missing threshold (verifyManifest would
|
|
61
|
+
// accept an unsigned manifest at threshold 0). The composition root rejects
|
|
62
|
+
// threshold < 1 before wiring, but guard the policy directly too.
|
|
63
|
+
if (threshold < 1) {
|
|
64
|
+
logger.error("manifest.http.poll.failed", { reason: "manifest_threshold_invalid", threshold, correlationId });
|
|
65
|
+
return { ok: false, reason: "manifest_threshold_invalid" };
|
|
66
|
+
}
|
|
67
|
+
// 4. Threshold signature (RFC 8032) against locally-pinned root keys.
|
|
68
|
+
const verifyResult = verifyManifest(manifest, rootKeys, threshold);
|
|
69
|
+
if (!verifyResult.ok) {
|
|
70
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_signature_invalid", manifestVersion: manifest.version, detail: verifyResult.reason, correlationId });
|
|
71
|
+
return { ok: false, reason: "manifest_signature_invalid" };
|
|
72
|
+
}
|
|
73
|
+
// 5. Validity window.
|
|
74
|
+
if (now < new Date(manifest.not_before)) {
|
|
75
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_not_yet_valid", manifestVersion: manifest.version, notBefore: manifest.not_before, correlationId });
|
|
76
|
+
return { ok: false, reason: "manifest_not_yet_valid" };
|
|
77
|
+
}
|
|
78
|
+
if (new Date(manifest.expires) <= now) {
|
|
79
|
+
logger.error("manifest.http.poll.failed", { reason: "manifest_expired", manifestVersion: manifest.version, expiresAt: manifest.expires, correlationId });
|
|
80
|
+
return { ok: false, reason: "manifest_expired" };
|
|
81
|
+
}
|
|
82
|
+
// 6-7. Anti-rollback + adopt. The version store / provider are I/O (the version store is
|
|
83
|
+
// DB-backed in production), so they can THROW — wrap them so a store error becomes a distinct,
|
|
84
|
+
// LOGGED failure (manifest_store_error) instead of an invisible throw that silently disables
|
|
85
|
+
// manifest adoption forever (fallback-finder MED).
|
|
86
|
+
try {
|
|
87
|
+
const lastSeen = await manifestVersionStore.getLastSeenVersion();
|
|
88
|
+
if (lastSeen !== null && manifest.version < lastSeen) {
|
|
89
|
+
logger.warn("manifest.http.poll.failed", { reason: "manifest_version_rollback", manifestVersion: manifest.version, lastSeenVersion: lastSeen, correlationId });
|
|
90
|
+
return { ok: false, reason: "manifest_version_rollback" };
|
|
91
|
+
}
|
|
92
|
+
if (lastSeen !== null && manifest.version === lastSeen) {
|
|
93
|
+
// Already current — nothing to adopt.
|
|
94
|
+
return { ok: true, adopted: false, oldVersion: lastSeen, newVersion: manifest.version };
|
|
95
|
+
}
|
|
96
|
+
const oldVersion = manifestProvider.getCurrentManifest()?.version ?? null;
|
|
97
|
+
manifestProvider.updateManifest(manifest);
|
|
98
|
+
await manifestVersionStore.persistVersion(manifest.version);
|
|
99
|
+
logger.info("manifest.http.poll.success", { oldVersion, newVersion: manifest.version, directoryUrl, correlationId });
|
|
100
|
+
return { ok: true, adopted: true, oldVersion, newVersion: manifest.version };
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.error("manifest.http.poll.failed", { reason: "manifest_store_error", manifestVersion: manifest.version, directoryUrl, correlationId, error: err instanceof Error ? err.message : String(err) });
|
|
104
|
+
return { ok: false, reason: "manifest_store_error" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wire the daemon-level HTTP manifest poll onto a scheduler. Self-rearming: each tick
|
|
109
|
+
* polls then schedules the next. Returns a stop function. The poll runs regardless of
|
|
110
|
+
* how many agents exist (including zero) — it has no agent identity and opens no
|
|
111
|
+
* signaling connection.
|
|
112
|
+
*/
|
|
113
|
+
export function startHttpManifestPoll(opts) {
|
|
114
|
+
const { scheduler, directoryUrl, fetchFn, mintCorrelationId, manifestProvider, manifestVersionStore, rootKeys, threshold, logger } = opts;
|
|
115
|
+
let stopped = false;
|
|
116
|
+
const tick = async () => {
|
|
117
|
+
if (stopped)
|
|
118
|
+
return;
|
|
119
|
+
await pollManifestOverHttp({
|
|
120
|
+
directoryUrl,
|
|
121
|
+
fetchFn,
|
|
122
|
+
correlationId: mintCorrelationId?.(),
|
|
123
|
+
manifestProvider,
|
|
124
|
+
manifestVersionStore,
|
|
125
|
+
rootKeys,
|
|
126
|
+
threshold,
|
|
127
|
+
logger,
|
|
128
|
+
}).catch((err) => {
|
|
129
|
+
// pollManifestOverHttp is designed never to throw (all failures return a reason). If it ever
|
|
130
|
+
// does, LOG it (never swallow silently — that would hide a permanently-disabled poll) and let
|
|
131
|
+
// the loop self-heal by re-arming below.
|
|
132
|
+
logger.error("manifest.http.poll.failed", { reason: "manifest_poll_unexpected_error", directoryUrl, error: err instanceof Error ? err.message : String(err) });
|
|
133
|
+
});
|
|
134
|
+
if (!stopped)
|
|
135
|
+
scheduler.scheduleNext(tick);
|
|
136
|
+
};
|
|
137
|
+
scheduler.scheduleNext(tick);
|
|
138
|
+
return () => {
|
|
139
|
+
stopped = true;
|
|
140
|
+
scheduler.cancel();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=http-manifest-poll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-manifest-poll.js","sourceRoot":"","sources":["../src/http-manifest-poll.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync, readFileSync, readdirSync, statSync, renameSync, unlinkSync, } from "node:fs";
|
|
25
25
|
import { join, dirname } from "node:path";
|
|
26
|
-
import { createDecipheriv } from "node:crypto";
|
|
26
|
+
import { createDecipheriv, randomUUID } from "node:crypto";
|
|
27
27
|
import { DatabaseSync } from "node:sqlite";
|
|
28
28
|
import { InMemoryKeyProvider, decodeKeyFileSeed } from "@cello-protocol/crypto";
|
|
29
29
|
import { openEncryptedDatabase, isPlaintextSqliteFile, resolveDbKey, dbKeyPathFor, } from "./sqlcipher-db.js";
|
|
@@ -357,10 +357,15 @@ function importFlatIdentity(celloDir, encDb, logger) {
|
|
|
357
357
|
const agents = discoverFlatAgents(celloDir);
|
|
358
358
|
let count = 0;
|
|
359
359
|
for (const a of agents) {
|
|
360
|
-
// Skip an agent
|
|
361
|
-
// a cleanup unlink failed) — never overwrite live DB identity with stale flat-file
|
|
362
|
-
// (fallback-finder / code-review LOW).
|
|
363
|
-
|
|
360
|
+
// Skip an agent whose name is held by an ACTIVE row in the encrypted DB (e.g. a prior in-place run
|
|
361
|
+
// imported it but a cleanup unlink failed) — never overwrite live DB identity with stale flat-file
|
|
362
|
+
// values (fallback-finder / code-review LOW). REMOVE-001: a RETIRED tombstone does NOT occupy the
|
|
363
|
+
// name (it was freed for reuse), so qualify active-only — matching hasActiveAgent everywhere else —
|
|
364
|
+
// otherwise a genuinely different flat identity restored under a freed name would be silently
|
|
365
|
+
// skipped and then deleted. deleteFlatIdentity will remove the redundant files.
|
|
366
|
+
const exists = encDb
|
|
367
|
+
.prepare("SELECT 1 AS one FROM agents WHERE agent_name = ? AND state != 'retired'")
|
|
368
|
+
.get(a.name);
|
|
364
369
|
if (exists)
|
|
365
370
|
continue;
|
|
366
371
|
// K_local seed from the 37-byte CELLO key file. A corrupt key SKIPS the WHOLE agent — its key AND
|
|
@@ -386,10 +391,11 @@ function importFlatIdentity(celloDir, encDb, logger) {
|
|
|
386
391
|
const link = readJson(join(a.dir, REG_FILES.link));
|
|
387
392
|
const state = reg ? "registered" : "created";
|
|
388
393
|
const now = Date.now();
|
|
394
|
+
// REMOVE-001: the store is keyed by a stable agent_id — mint one for each imported flat agent.
|
|
389
395
|
encDb
|
|
390
|
-
.prepare(`INSERT OR IGNORE INTO agents (agent_name, k_local_seed, k_local_pubkey, state, created_at, updated_at)
|
|
391
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
392
|
-
.run(a.name, Buffer.from(seed), pubkeyHex, state, now, now);
|
|
396
|
+
.prepare(`INSERT OR IGNORE INTO agents (agent_id, agent_name, k_local_seed, k_local_pubkey, state, created_at, updated_at)
|
|
397
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
398
|
+
.run(randomUUID(), a.name, Buffer.from(seed), pubkeyHex, state, now, now);
|
|
393
399
|
if (mlDsa) {
|
|
394
400
|
encDb
|
|
395
401
|
.prepare("UPDATE agents SET ml_dsa_pubkey = ?, ml_dsa_secret = ?, ml_dsa_algorithm = ? WHERE agent_name = ?")
|