@tpsdev-ai/flair 0.52.0 → 0.54.1

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.
Files changed (99) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1914 -15328
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1654 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +914 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1592 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/fabric-npm-install.js +87 -0
  43. package/dist/federation-verify.js +498 -0
  44. package/dist/fleet-verify.js +144 -21
  45. package/dist/install/clients.js +167 -0
  46. package/dist/lib/auth-resolve.js +76 -1
  47. package/dist/lib/daemon-liveness.js +131 -2
  48. package/dist/lib/doctor-config-path.js +61 -0
  49. package/dist/lib/doctor-federation-driver.js +189 -0
  50. package/dist/lib/doctor-run.js +40 -0
  51. package/dist/lib/entity-vocab-cli.js +3 -3
  52. package/dist/lib/federation-pair-identity.js +47 -0
  53. package/dist/lib/launchd-repair.js +199 -0
  54. package/dist/lib/ops-api-bind.js +115 -0
  55. package/dist/lib/owned-pins.js +219 -0
  56. package/dist/lib/stabilize-mqtt-network.js +123 -0
  57. package/dist/lib/uninstall-purge.js +218 -0
  58. package/dist/rem/restore.js +8 -10
  59. package/dist/resources/AgentReadPosition.js +74 -0
  60. package/dist/resources/Federation.js +8 -2
  61. package/dist/resources/Memory.js +4 -3
  62. package/dist/resources/MemoryBootstrap.js +41 -25
  63. package/dist/resources/MemoryCandidate.js +5 -6
  64. package/dist/resources/OrgEventCatchup.js +126 -47
  65. package/dist/resources/agent-read-position-lib.js +83 -0
  66. package/dist/resources/agent-read-position.js +120 -0
  67. package/dist/resources/embeddings-boot.js +32 -0
  68. package/dist/resources/federation-peer-liveness.js +73 -0
  69. package/dist/resources/health.js +68 -19
  70. package/dist/resources/mcp-tools.js +43 -279
  71. package/dist/resources/memory-visibility.js +3 -3
  72. package/dist/resources/migration-boot.js +59 -18
  73. package/dist/resources/migrations/embedding-stamp.js +20 -1
  74. package/dist/resources/migrations/recheck.js +43 -0
  75. package/dist/resources/migrations/runner.js +6 -1
  76. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  77. package/dist/resources/migrations/visibility-backfill.js +2 -2
  78. package/dist/resources/org-event-catchup-lib.js +47 -0
  79. package/dist/resources/record-owner-guard.js +1 -0
  80. package/dist/stamp-migration-verify.js +163 -0
  81. package/dist/stamp-outstanding.js +144 -0
  82. package/docs/api-reference.md +4 -2
  83. package/docs/deploying-on-fabric.md +11 -10
  84. package/docs/deployment.md +3 -1
  85. package/docs/federation.md +19 -0
  86. package/docs/hosted-on-fabric.md +3 -3
  87. package/docs/quickstart.md +2 -1
  88. package/docs/releasing.md +15 -7
  89. package/docs/spoke-bringup.md +10 -5
  90. package/docs/standalone-local.md +3 -1
  91. package/docs/upgrade.md +25 -6
  92. package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
  93. package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
  94. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
  95. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
  96. package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
  97. package/package.json +9 -4
  98. package/schemas/agent.graphql +15 -0
  99. package/templates/launchd/start-flair-with-admin-pass.sh +73 -0
@@ -0,0 +1,338 @@
1
+ import nacl from "tweetnacl";
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from "node:fs";
3
+ import * as render from "../render.js";
4
+ import { defaultKeysDir, resolveLocalAdminPass, resolveAdminUser, } from "../lib/auth-resolve.js";
5
+ let cli;
6
+ /** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
7
+ export function bindCli(fns) {
8
+ cli = fns;
9
+ }
10
+ const api = (method, path, body, options) => cli.api(method, path, body, options);
11
+ const b64url = (bytes) => cli.b64url(bytes);
12
+ const privKeyPath = (agentId, keysDir) => cli.privKeyPath(agentId, keysDir);
13
+ const pubKeyPath = (agentId, keysDir) => cli.pubKeyPath(agentId, keysDir);
14
+ const relativeTime = (iso) => cli.relativeTime(iso);
15
+ const resolveOpsPort = (opts) => cli.resolveOpsPort(opts);
16
+ // ─── flair principal ─────────────────────────────────────────────────────────
17
+ // 1.0 identity management. The Principal model extends Agent — this is the
18
+ // preferred CLI surface for managing identities going forward.
19
+ /**
20
+ * The exact `role` value that denotes a flair administrator, and the predicate
21
+ * that reads it.
22
+ *
23
+ * DUPLICATED FROM resources/agent-admin.ts on purpose — the same deliberate
24
+ * copy as the federation crypto helpers above: src/cli.ts must not import from
25
+ * resources/, because those imports don't survive npm packaging. The two must
26
+ * stay in sync.
27
+ *
28
+ * flair#941: `role` is the authority and `admin` is its mirror. The CLI used to
29
+ * both write and display ONLY the mirror, so `principal add --admin` created a
30
+ * principal the gate refuses and `principal show` printed "admin: yes" for it.
31
+ */
32
+ const ADMIN_ROLE = "admin";
33
+ export function agentRecordIsAdmin(record) {
34
+ return record?.role === ADMIN_ROLE;
35
+ }
36
+ /** Register the `flair principal` command group (flair#1632). */
37
+ export function register(program) {
38
+ const principal = program.command("principal").description("Manage principals (humans and agents)");
39
+ principal
40
+ .command("add <id>")
41
+ .description("Create a new principal")
42
+ .option("--kind <kind>", "Principal kind: human or agent", "agent")
43
+ .option("--name <name>", "Display name (defaults to id)")
44
+ .option("--admin", "Grant admin privileges")
45
+ .option("--trust <tier>", "Default trust tier: endorsed, corroborated, or unverified")
46
+ .option("--runtime <runtime>", "Runtime: openclaw, claude-code, headless, external")
47
+ .option("--port <port>", "Harper HTTP port")
48
+ .option("--admin-pass <pass>", "Admin password for registration")
49
+ .option("--admin-user <name>", "Admin username for Basic auth (env: FLAIR_ADMIN_USER; default: admin)")
50
+ .option("--keys-dir <dir>", "Directory for Ed25519 keys")
51
+ .option("--ops-port <port>", "Harper operations API port")
52
+ .action(async (id, opts) => {
53
+ const opsPort = resolveOpsPort(opts);
54
+ const keysDir = opts.keysDir ?? defaultKeysDir();
55
+ const adminUser = resolveAdminUser(opts.adminUser);
56
+ const kind = opts.kind ?? "agent";
57
+ const name = opts.name ?? id;
58
+ const isAdmin = opts.admin ?? false;
59
+ const trustTier = opts.trust ?? (isAdmin ? "endorsed" : "unverified");
60
+ const runtime = opts.runtime;
61
+ // #590 — same local-only fallback as `agent add`: FLAIR_ADMIN_PASS env, then
62
+ // the secure ~/.flair/admin-pass file (mode 0600). `principal add` has no
63
+ // --target/--ops-target (always localhost), so the fallback always applies.
64
+ let adminPass;
65
+ try {
66
+ adminPass = resolveLocalAdminPass(opts.adminPass);
67
+ }
68
+ catch (err) {
69
+ console.error(`Error: ${err.message}`);
70
+ process.exit(1);
71
+ }
72
+ if (!adminPass) {
73
+ console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required (or ensure ~/.flair/admin-pass exists, " +
74
+ "created by `flair init`)");
75
+ process.exit(1);
76
+ }
77
+ // Generate Ed25519 keypair (agents always get one; humans get one for instance-attestation)
78
+ mkdirSync(keysDir, { recursive: true });
79
+ const privPath = privKeyPath(id, keysDir);
80
+ const pubPath = pubKeyPath(id, keysDir);
81
+ let pubKeyB64url;
82
+ if (existsSync(privPath)) {
83
+ console.log(`Reusing existing key: ${privPath}`);
84
+ const seed = new Uint8Array(readFileSync(privPath));
85
+ const kp = nacl.sign.keyPair.fromSeed(seed);
86
+ pubKeyB64url = b64url(kp.publicKey);
87
+ }
88
+ else {
89
+ const kp = nacl.sign.keyPair();
90
+ const seed = kp.secretKey.slice(0, 32);
91
+ writeFileSync(privPath, Buffer.from(seed));
92
+ chmodSync(privPath, 0o600);
93
+ writeFileSync(pubPath, Buffer.from(kp.publicKey));
94
+ pubKeyB64url = b64url(kp.publicKey);
95
+ console.log(`Keypair written: ${privPath}`);
96
+ }
97
+ // Insert via operations API with Principal fields
98
+ const auth = `Basic ${Buffer.from(`${adminUser}:${adminPass}`).toString("base64")}`;
99
+ const record = {
100
+ id,
101
+ name,
102
+ displayName: name,
103
+ kind,
104
+ type: kind === "human" ? "human" : "agent",
105
+ status: "active",
106
+ publicKey: pubKeyB64url,
107
+ defaultTrustTier: trustTier,
108
+ // flair#941 — write BOTH. This is an ops-API upsert, so the Agent
109
+ // resource's reconciliation never runs; writing only the `admin` mirror
110
+ // is what made `--admin` a no-op at the gate for every principal this
111
+ // command has ever created.
112
+ role: isAdmin ? ADMIN_ROLE : "agent",
113
+ admin: isAdmin,
114
+ runtime: runtime ?? null,
115
+ createdAt: new Date().toISOString(),
116
+ updatedAt: new Date().toISOString(),
117
+ };
118
+ const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json", Authorization: auth },
121
+ body: JSON.stringify({ operation: "upsert", database: "flair", table: "Agent", records: [record] }),
122
+ });
123
+ if (!res.ok) {
124
+ const text = await res.text().catch(() => "");
125
+ console.error(`Error: ${res.status} ${text}`);
126
+ process.exit(1);
127
+ }
128
+ console.log(`✅ Principal '${id}' created`);
129
+ console.log(` Kind: ${kind}`);
130
+ console.log(` Trust: ${trustTier}`);
131
+ console.log(` Admin: ${isAdmin}`);
132
+ if (runtime)
133
+ console.log(` Runtime: ${runtime}`);
134
+ console.log(` Public key: ${pubKeyB64url}`);
135
+ console.log(` Private key: ${privPath}`);
136
+ });
137
+ principal
138
+ .command("list")
139
+ .description("List all principals")
140
+ .option("--kind <kind>", "Filter by kind: human or agent")
141
+ .option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS)")
142
+ .option("--admin-user <name>", "Admin username for Basic auth (env: FLAIR_ADMIN_USER; default: admin)")
143
+ .option("--port <port>", "Harper HTTP port")
144
+ .option("--ops-port <port>", "Harper operations API port")
145
+ .option("--json", "Emit raw JSON array (also: pipe + FLAIR_OUTPUT=json)")
146
+ .action(async (opts) => {
147
+ const opsPort = resolveOpsPort(opts);
148
+ const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
149
+ if (!adminPass) {
150
+ console.error(`${render.icons.error} --admin-pass or FLAIR_ADMIN_PASS required`);
151
+ process.exit(1);
152
+ }
153
+ const auth = `Basic ${Buffer.from(`${resolveAdminUser(opts.adminUser)}:${adminPass}`).toString("base64")}`;
154
+ const conditions = opts.kind
155
+ ? [{ search_attribute: "kind", search_type: "equals", search_value: opts.kind }]
156
+ : [{ search_attribute: "id", search_type: "starts_with", search_value: "" }];
157
+ const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
158
+ method: "POST",
159
+ headers: { "Content-Type": "application/json", Authorization: auth },
160
+ body: JSON.stringify({
161
+ operation: "search_by_conditions",
162
+ schema: "flair",
163
+ table: "Agent",
164
+ operator: "and",
165
+ conditions,
166
+ // `role` is the authority behind admin status (flair#941); the
167
+ // projection used to omit it, so this listing could only ever report
168
+ // the mirror.
169
+ get_attributes: ["id", "name", "kind", "status", "defaultTrustTier", "role", "admin", "runtime", "createdAt"],
170
+ }),
171
+ });
172
+ if (!res.ok) {
173
+ const text = await res.text().catch(() => "");
174
+ console.error(`${render.icons.error} ${res.status} ${text}`);
175
+ process.exit(1);
176
+ }
177
+ const records = await res.json();
178
+ records.sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? ""));
179
+ const mode = render.resolveOutputMode(opts);
180
+ if (mode === "json") {
181
+ console.log(render.asJSON(records));
182
+ return;
183
+ }
184
+ if (records.length === 0) {
185
+ console.log(`${render.icons.info} ${render.wrap(render.c.dim, "no principals")}`);
186
+ return;
187
+ }
188
+ console.log(`${render.wrap(render.c.bold, String(records.length))} principals${opts.kind ? ` ${render.wrap(render.c.dim, `(kind=${opts.kind})`)}` : ""}\n`);
189
+ const cols = [
190
+ { label: "id", key: "id", format: (v) => render.wrap(render.c.bold, String(v ?? "—")) },
191
+ {
192
+ label: "kind",
193
+ key: "kind",
194
+ format: (v) => {
195
+ const k = String(v ?? "agent");
196
+ return render.wrap(k === "human" ? render.c.cyan : render.c.magenta, k);
197
+ },
198
+ },
199
+ { label: "trust", key: "defaultTrustTier", format: (v) => String(v ?? "—") },
200
+ {
201
+ label: "admin",
202
+ key: "admin",
203
+ // Report the status the gate will apply, and flag a record whose two
204
+ // fields disagree rather than picking a side silently (flair#941).
205
+ format: (_v, row) => {
206
+ const isAdmin = agentRecordIsAdmin(row);
207
+ const mismatch = isAdmin !== (row.admin === true);
208
+ const base = isAdmin ? render.wrap(render.c.red, "yes") : render.wrap(render.c.dim, "no");
209
+ return mismatch ? `${base} ${render.wrap(render.c.yellow, "(!)")}` : base;
210
+ },
211
+ },
212
+ {
213
+ label: "status",
214
+ key: "status",
215
+ format: (v) => {
216
+ const s = String(v ?? "active");
217
+ const color = s === "active" ? render.c.green : s === "disabled" ? render.c.red : render.c.yellow;
218
+ return render.wrap(color, s);
219
+ },
220
+ },
221
+ { label: "runtime", key: "runtime", format: (v) => String(v ?? "—") },
222
+ { label: "created", key: "createdAt", format: (v) => render.wrap(render.c.dim, v ? String(v).slice(0, 10) : "—") },
223
+ ];
224
+ console.log(render.table(cols, records));
225
+ });
226
+ principal
227
+ .command("show <id>")
228
+ .description("Show principal details")
229
+ .option("--json", "Emit raw JSON response (also: pipe + FLAIR_OUTPUT=json)")
230
+ .action(async (id, opts) => {
231
+ const result = await api("GET", `/Agent/${id}`);
232
+ const mode = render.resolveOutputMode(opts);
233
+ if (mode === "json") {
234
+ console.log(render.asJSON(result));
235
+ return;
236
+ }
237
+ if (!result || (typeof result === "object" && !result.id)) {
238
+ console.log(`${render.icons.info} ${render.wrap(render.c.dim, `no principal ${id}`)}`);
239
+ return;
240
+ }
241
+ console.log(render.wrap(render.c.bold, String(result.id)));
242
+ if (result.name)
243
+ console.log(render.kv("name", String(result.name)));
244
+ if (result.kind)
245
+ console.log(render.kv("kind", render.wrap(result.kind === "human" ? render.c.cyan : render.c.magenta, String(result.kind))));
246
+ if (result.status) {
247
+ const statusColor = result.status === "active" ? render.c.green : result.status === "disabled" ? render.c.red : render.c.yellow;
248
+ console.log(render.kv("status", render.wrap(statusColor, String(result.status))));
249
+ }
250
+ if (result.defaultTrustTier)
251
+ console.log(render.kv("trust tier", String(result.defaultTrustTier)));
252
+ // flair#941 — read the authority, not the mirror, and say so when the two
253
+ // disagree (only reachable via a raw table write).
254
+ if (agentRecordIsAdmin(result))
255
+ console.log(render.kv("admin", render.wrap(render.c.red, "yes")));
256
+ if (agentRecordIsAdmin(result) !== (result.admin === true)) {
257
+ console.log(render.kv("admin", render.wrap(render.c.yellow, `record is inconsistent (role=${result.role ?? "unset"}, admin=${result.admin ?? "unset"}) — re-issue the grant to repair`)));
258
+ }
259
+ if (result.runtime)
260
+ console.log(render.kv("runtime", String(result.runtime)));
261
+ if (result.email)
262
+ console.log(render.kv("email", String(result.email)));
263
+ if (result.publicKey)
264
+ console.log(render.kv("publicKey", render.wrap(render.c.dim, String(result.publicKey))));
265
+ if (result.createdAt)
266
+ console.log(render.kv("created", `${render.relativeTime(result.createdAt)} ${render.wrap(render.c.dim, `(${result.createdAt})`)}`));
267
+ if (result.updatedAt && result.updatedAt !== result.createdAt) {
268
+ console.log(render.kv("updated", `${render.relativeTime(result.updatedAt)} ${render.wrap(render.c.dim, `(${result.updatedAt})`)}`));
269
+ }
270
+ });
271
+ principal
272
+ .command("disable <id>")
273
+ .description("Deactivate a principal (revokes access, preserves data)")
274
+ .option("--admin-pass <pass>", "Admin password")
275
+ .option("--admin-user <name>", "Admin username for Basic auth (env: FLAIR_ADMIN_USER; default: admin)")
276
+ .option("--ops-port <port>", "Harper operations API port")
277
+ .action(async (id, opts) => {
278
+ const opsPort = resolveOpsPort(opts);
279
+ const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
280
+ if (!adminPass) {
281
+ console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
282
+ process.exit(1);
283
+ }
284
+ const auth = `Basic ${Buffer.from(`${resolveAdminUser(opts.adminUser)}:${adminPass}`).toString("base64")}`;
285
+ const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
286
+ method: "POST",
287
+ headers: { "Content-Type": "application/json", Authorization: auth },
288
+ body: JSON.stringify({
289
+ operation: "update",
290
+ database: "flair",
291
+ table: "Agent",
292
+ records: [{ id, status: "deactivated", updatedAt: new Date().toISOString() }],
293
+ }),
294
+ });
295
+ if (!res.ok) {
296
+ const text = await res.text().catch(() => "");
297
+ console.error(`Error: ${res.status} ${text}`);
298
+ process.exit(1);
299
+ }
300
+ console.log(`✅ Principal '${id}' deactivated`);
301
+ });
302
+ principal
303
+ .command("promote <id> <tier>")
304
+ .description("Change a principal's trust tier (endorsed, corroborated, unverified)")
305
+ .option("--admin-pass <pass>", "Admin password")
306
+ .option("--admin-user <name>", "Admin username for Basic auth (env: FLAIR_ADMIN_USER; default: admin)")
307
+ .option("--ops-port <port>", "Harper operations API port")
308
+ .action(async (id, tier, opts) => {
309
+ const validTiers = ["endorsed", "corroborated", "unverified"];
310
+ if (!validTiers.includes(tier)) {
311
+ console.error(`Error: tier must be one of: ${validTiers.join(", ")}`);
312
+ process.exit(1);
313
+ }
314
+ const opsPort = resolveOpsPort(opts);
315
+ const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
316
+ if (!adminPass) {
317
+ console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
318
+ process.exit(1);
319
+ }
320
+ const auth = `Basic ${Buffer.from(`${resolveAdminUser(opts.adminUser)}:${adminPass}`).toString("base64")}`;
321
+ const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json", Authorization: auth },
324
+ body: JSON.stringify({
325
+ operation: "update",
326
+ database: "flair",
327
+ table: "Agent",
328
+ records: [{ id, defaultTrustTier: tier, updatedAt: new Date().toISOString() }],
329
+ }),
330
+ });
331
+ if (!res.ok) {
332
+ const text = await res.text().catch(() => "");
333
+ console.error(`Error: ${res.status} ${text}`);
334
+ process.exit(1);
335
+ }
336
+ console.log(`✅ Principal '${id}' trust tier set to '${tier}'`);
337
+ });
338
+ }