@securecoms/agent-bridge 0.7.0 → 0.9.0

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.
@@ -842,6 +842,10 @@ export declare class BridgeAgent {
842
842
  hermes_write_failed?: number;
843
843
  openclaw_no_session?: number;
844
844
  openclaw_read_failed?: number;
845
+ openclaw_facts_missing?: number;
846
+ openclaw_facts_read_failed?: number;
847
+ openclaw_facts_truncated?: number;
848
+ openclaw_facts_chars?: number;
845
849
  };
846
850
  /**
847
851
  * Loopback observability snapshot served at GET /v1/metrics.
package/dist/config.d.ts CHANGED
@@ -23,6 +23,10 @@ export interface BridgeConfig {
23
23
  memoryBaseDir?: string;
24
24
  /** For memoryProvider="openclaw": the OpenClaw agent dir slug, e.g. "cortina" (→ <base_dir>/agents/<agent>/). */
25
25
  memoryAgent?: string;
26
+ /** For memoryProvider="openclaw": explicit workspace dir override; wins over openclaw.json (spec D4 rung 1). */
27
+ memoryWorkspaceDir?: string;
28
+ /** For memoryProvider="openclaw": total facts cap in UTF-16 code units. 0 = facts kill switch (preserved via ?? downstream, never ||). Absent/junk → provider default 8000. */
29
+ memoryFactsCharBudget?: number;
26
30
  }
27
31
  export declare const REQUIRED: readonly ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
28
32
  export declare function loadConfig(opts: {
@@ -34,7 +34,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
34
34
  message_id: z.ZodOptional<z.ZodString>;
35
35
  idempotency_key: z.ZodOptional<z.ZodString>;
36
36
  }, "strip", z.ZodTypeAny, {
37
- role: "peer" | "agent";
37
+ role: "agent" | "peer";
38
38
  text: string;
39
39
  conversation_id: string;
40
40
  modality: "text" | "voice";
@@ -42,7 +42,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
42
42
  idempotency_key?: string | undefined;
43
43
  peer_id?: string | undefined;
44
44
  }, {
45
- role: "peer" | "agent";
45
+ role: "agent" | "peer";
46
46
  text: string;
47
47
  conversation_id: string;
48
48
  modality: "text" | "voice";
@@ -50,7 +50,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
50
50
  idempotency_key?: string | undefined;
51
51
  peer_id?: string | undefined;
52
52
  }>, {
53
- role: "peer" | "agent";
53
+ role: "agent" | "peer";
54
54
  text: string;
55
55
  conversation_id: string;
56
56
  modality: "text" | "voice";
@@ -58,7 +58,7 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
58
58
  idempotency_key?: string | undefined;
59
59
  peer_id?: string | undefined;
60
60
  }, {
61
- role: "peer" | "agent";
61
+ role: "agent" | "peer";
62
62
  text: string;
63
63
  conversation_id: string;
64
64
  modality: "text" | "voice";
package/dist/index.js CHANGED
@@ -3811,7 +3811,7 @@ function loadPackageMeta(importMetaUrl) {
3811
3811
 
3812
3812
  // src/bridge-agent.ts
3813
3813
  import { promises as fsp } from "node:fs";
3814
- import { join as join5, dirname } from "node:path";
3814
+ import { join as join6, dirname } from "node:path";
3815
3815
 
3816
3816
  // src/latency.ts
3817
3817
  var DEFAULT_CAPACITY = 1024;
@@ -4324,29 +4324,153 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
4324
4324
  return k > 0 ? turns.slice(-k) : [];
4325
4325
  }
4326
4326
 
4327
+ // src/memory/openclaw-workspace-facts.ts
4328
+ import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
4329
+ import { join as join5 } from "node:path";
4330
+ var DEFAULT_FACTS_BUDGET = 8e3;
4331
+ var MEMORY_MD_CAP = 6e3;
4332
+ var MIN_FRAGMENT = 200;
4333
+ var MAX_SOURCE_BYTES = 1024 * 1024;
4334
+ var DAILY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
4335
+ function resolveWorkspaceDir(baseDir, agent, override) {
4336
+ if (override) return expandHome(override);
4337
+ const base = expandHome(baseDir);
4338
+ const isMain = agent === "" || agent === "main";
4339
+ try {
4340
+ const cfg = JSON.parse(readFileSync4(join5(base, "openclaw.json"), "utf8"));
4341
+ const agents = cfg?.agents;
4342
+ if (agents && typeof agents === "object") {
4343
+ const list = Array.isArray(agents.list) ? agents.list : [];
4344
+ const wanted = isMain ? "main" : agent;
4345
+ const entry = list.find((e) => e && typeof e === "object" && e.id === wanted);
4346
+ const ws = entry?.workspace;
4347
+ if (typeof ws === "string" && ws) return expandHome(ws);
4348
+ const def = agents.defaults?.workspace;
4349
+ if (typeof def === "string" && def) return expandHome(def);
4350
+ }
4351
+ } catch {
4352
+ }
4353
+ return isMain ? join5(base, "workspace") : join5(base, `workspace-${agent}`);
4354
+ }
4355
+ var TRUNC = "\u2026[truncated]";
4356
+ function headSlice(s, n) {
4357
+ if (s.length <= n) return { text: s, clipped: false };
4358
+ let out = s.slice(0, n);
4359
+ const last = out.charCodeAt(out.length - 1);
4360
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
4361
+ return { text: out, clipped: true };
4362
+ }
4363
+ function tailSlice(s, n) {
4364
+ if (s.length <= n) return { text: s, clipped: false };
4365
+ let out = s.slice(s.length - n);
4366
+ const first = out.charCodeAt(0);
4367
+ if (first >= 56320 && first <= 57343) out = out.slice(1);
4368
+ return { text: out, clipped: true };
4369
+ }
4370
+ function readBounded(path) {
4371
+ if (statSync(path).size > MAX_SOURCE_BYTES) return { text: null, oversize: true };
4372
+ return { text: readFileSync4(path, "utf8"), oversize: false };
4373
+ }
4374
+ function readWorkspaceFacts(baseDir, agent, opts) {
4375
+ const raw = opts?.charBudget;
4376
+ const budget = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_FACTS_BUDGET;
4377
+ if (budget === 0) return { facts: [], truncated: false, missing: false };
4378
+ const dir = resolveWorkspaceDir(baseDir, agent, opts?.workspaceDir);
4379
+ const facts = [];
4380
+ let truncated = false;
4381
+ let remaining = budget;
4382
+ try {
4383
+ const r = readBounded(join5(dir, "MEMORY.md"));
4384
+ if (r.oversize) truncated = true;
4385
+ else if (r.text && r.text.trim()) {
4386
+ const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
4387
+ truncated ||= clipped;
4388
+ facts.push(`workspace MEMORY.md:
4389
+ ${text}${clipped ? `
4390
+ ${TRUNC}` : ""}`);
4391
+ remaining -= text.length;
4392
+ }
4393
+ } catch {
4394
+ }
4395
+ try {
4396
+ const dailies = readdirSync(join5(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
4397
+ for (const f of dailies) {
4398
+ if (remaining < MIN_FRAGMENT) {
4399
+ truncated = true;
4400
+ break;
4401
+ }
4402
+ let r;
4403
+ try {
4404
+ r = readBounded(join5(dir, "memory", f));
4405
+ } catch {
4406
+ continue;
4407
+ }
4408
+ if (r.oversize) {
4409
+ truncated = true;
4410
+ continue;
4411
+ }
4412
+ if (!r.text || !r.text.trim()) continue;
4413
+ const { text, clipped } = tailSlice(r.text, remaining);
4414
+ truncated ||= clipped;
4415
+ facts.push(`workspace memory/${f} (daily):
4416
+ ${clipped ? `${TRUNC}
4417
+ ` : ""}${text}`);
4418
+ remaining -= text.length;
4419
+ }
4420
+ } catch {
4421
+ }
4422
+ return { facts, truncated, missing: facts.length === 0 };
4423
+ }
4424
+
4327
4425
  // src/memory/openclaw-provider.ts
4328
4426
  var DEFAULT_K2 = 200;
4329
4427
  var OpenClawMemoryProvider = class {
4330
- constructor(store, baseDir, agent) {
4428
+ // gauge: last successful returned total, including prefixes/markers
4429
+ constructor(store, baseDir, agent, factsOpts) {
4331
4430
  this.store = store;
4332
4431
  this.baseDir = baseDir;
4333
4432
  this.agent = agent;
4433
+ this.factsOpts = factsOpts;
4334
4434
  }
4335
4435
  noSession = 0;
4336
4436
  readFailed = 0;
4437
+ factsMissing = 0;
4438
+ // absence-shaped: resolved path missing or zero sources (per-call)
4439
+ factsReadFailed = 0;
4440
+ // anything THROWN out of the facts computation
4441
+ factsTruncated = 0;
4442
+ // budget clipped/dropped a source (incl. the 1MB skip)
4443
+ factsChars = 0;
4337
4444
  async record(input) {
4338
4445
  if (input.modality !== "voice") return { seq: 0, deduped: true };
4339
4446
  return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
4340
4447
  }
4341
4448
  async context(conversationId, limit) {
4342
4449
  const k = limit ?? DEFAULT_K2;
4450
+ let wsFacts = [];
4451
+ try {
4452
+ const r = readWorkspaceFacts(this.baseDir, this.agent, {
4453
+ workspaceDir: this.factsOpts?.workspaceDir,
4454
+ charBudget: this.factsOpts?.factsCharBudget
4455
+ });
4456
+ wsFacts = r.facts;
4457
+ if (r.missing) this.factsMissing++;
4458
+ if (r.truncated) this.factsTruncated++;
4459
+ this.factsChars = r.facts.reduce((n, f) => n + f.length, 0);
4460
+ } catch {
4461
+ this.factsReadFailed++;
4462
+ }
4463
+ const withFacts = (res) => ({
4464
+ recent: res.recent,
4465
+ facts: [...wsFacts, ...res.facts]
4466
+ });
4343
4467
  const voice = await this.store.memoryContext(conversationId, k);
4344
4468
  let textTurns = [];
4345
4469
  try {
4346
4470
  const sid = resolveOwnerDmSession(this.baseDir, this.agent);
4347
4471
  if (!sid) {
4348
4472
  this.noSession++;
4349
- return voice;
4473
+ return withFacts(voice);
4350
4474
  }
4351
4475
  textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
4352
4476
  seq: 0,
@@ -4356,7 +4480,7 @@ var OpenClawMemoryProvider = class {
4356
4480
  }));
4357
4481
  } catch {
4358
4482
  this.readFailed++;
4359
- return voice;
4483
+ return withFacts(voice);
4360
4484
  }
4361
4485
  const tagged = [
4362
4486
  ...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
@@ -4375,10 +4499,17 @@ var OpenClawMemoryProvider = class {
4375
4499
  text: t.text,
4376
4500
  ts: t.ts
4377
4501
  }));
4378
- return { recent, facts: voice.facts };
4502
+ return withFacts({ recent, facts: voice.facts });
4379
4503
  }
4380
4504
  stats() {
4381
- return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
4505
+ return {
4506
+ openclaw_no_session: this.noSession,
4507
+ openclaw_read_failed: this.readFailed,
4508
+ openclaw_facts_missing: this.factsMissing,
4509
+ openclaw_facts_read_failed: this.factsReadFailed,
4510
+ openclaw_facts_truncated: this.factsTruncated,
4511
+ openclaw_facts_chars: this.factsChars
4512
+ };
4382
4513
  }
4383
4514
  };
4384
4515
 
@@ -4402,10 +4533,10 @@ var NullMemory = class {
4402
4533
  return { recent: [], facts: [] };
4403
4534
  }
4404
4535
  };
4405
- function createMemoryProvider(provider, store, baseDir, agent) {
4536
+ function createMemoryProvider(provider, store, baseDir, agent, openclawFacts) {
4406
4537
  if (provider === "none") return new NullMemory();
4407
4538
  if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
4408
- if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
4539
+ if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "", openclawFacts);
4409
4540
  return new SecureComsLocalMemory(store);
4410
4541
  }
4411
4542
 
@@ -4513,7 +4644,10 @@ var BridgeAgent = class _BridgeAgent {
4513
4644
  identityId: cfg.identityId,
4514
4645
  cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
4515
4646
  });
4516
- this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
4647
+ this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent, {
4648
+ workspaceDir: this.cfg.memoryWorkspaceDir,
4649
+ factsCharBudget: this.cfg.memoryFactsCharBudget
4650
+ });
4517
4651
  this.provider = new MlsGroupKeyProvider({
4518
4652
  store: this.store,
4519
4653
  backend: this.backend,
@@ -4593,7 +4727,7 @@ var BridgeAgent = class _BridgeAgent {
4593
4727
  const j = await res.json();
4594
4728
  return String(j.attachment_id);
4595
4729
  };
4596
- this.attachmentsDirImpl = deps.attachmentsDir ?? join5(dirname(cfg.storePath), "attachments");
4730
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join6(dirname(cfg.storePath), "attachments");
4597
4731
  this.action = {
4598
4732
  begin: (channelId) => this.actionBegin(channelId),
4599
4733
  start: (opts) => this.actionStart(opts),
@@ -6120,13 +6254,13 @@ var BridgeAgent = class _BridgeAgent {
6120
6254
  };
6121
6255
 
6122
6256
  // src/config.ts
6123
- import { readFileSync as readFileSync4, statSync } from "node:fs";
6124
- import { dirname as dirname2, join as join6 } from "node:path";
6257
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
6258
+ import { dirname as dirname2, join as join7 } from "node:path";
6125
6259
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
6126
6260
  function loadConfig(opts) {
6127
- const raw = readFileSync4(opts.credentialPath, "utf8");
6261
+ const raw = readFileSync5(opts.credentialPath, "utf8");
6128
6262
  try {
6129
- const mode = statSync(opts.credentialPath).mode & 511;
6263
+ const mode = statSync2(opts.credentialPath).mode & 511;
6130
6264
  if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
6131
6265
  `);
6132
6266
  } catch {
@@ -6138,7 +6272,7 @@ function loadConfig(opts) {
6138
6272
  throw new Error("credential file is not valid JSON");
6139
6273
  }
6140
6274
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
6141
- const storePath = opts.storePath ?? join6(dirname2(opts.credentialPath), "bridge-state.db");
6275
+ const storePath = opts.storePath ?? join7(dirname2(opts.credentialPath), "bridge-state.db");
6142
6276
  const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
6143
6277
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
6144
6278
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
@@ -6146,6 +6280,8 @@ function loadConfig(opts) {
6146
6280
  let memoryProvider;
6147
6281
  let memoryBaseDir;
6148
6282
  let memoryAgent;
6283
+ let memoryWorkspaceDir;
6284
+ let memoryFactsCharBudget;
6149
6285
  if (memProvRaw === "none") {
6150
6286
  memoryProvider = "none";
6151
6287
  } else if (memProvRaw === "hermes") {
@@ -6159,6 +6295,10 @@ function loadConfig(opts) {
6159
6295
  memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
6160
6296
  const ag = oc?.agent;
6161
6297
  memoryAgent = typeof ag === "string" && ag ? ag : void 0;
6298
+ const wd = oc?.workspace_dir;
6299
+ memoryWorkspaceDir = typeof wd === "string" && wd ? wd : void 0;
6300
+ const fb = oc?.facts_char_budget;
6301
+ memoryFactsCharBudget = typeof fb === "number" && Number.isInteger(fb) && fb >= 0 ? fb : void 0;
6162
6302
  } else {
6163
6303
  memoryProvider = "securecoms";
6164
6304
  }
@@ -6175,7 +6315,9 @@ function loadConfig(opts) {
6175
6315
  rateLimitRefillPerMinute,
6176
6316
  memoryProvider,
6177
6317
  memoryBaseDir,
6178
- memoryAgent
6318
+ memoryAgent,
6319
+ memoryWorkspaceDir,
6320
+ memoryFactsCharBudget
6179
6321
  };
6180
6322
  }
6181
6323
  function arg(argv, flag) {
@@ -6219,11 +6361,11 @@ import { z } from "zod";
6219
6361
 
6220
6362
  // src/auth.ts
6221
6363
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
6222
- import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
6364
+ import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
6223
6365
  function resolveToken(opts) {
6224
6366
  if (opts.token) return opts.token;
6225
6367
  const f = opts.tokenFile;
6226
- if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
6368
+ if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
6227
6369
  const tok = randomBytes3(32).toString("base64url");
6228
6370
  if (f) writeFileSync(f, tok, { mode: 384 });
6229
6371
  return tok;
@@ -1,5 +1,9 @@
1
1
  import type { MemoryProvider } from "./provider.js";
2
2
  import type { SqliteMlsStateStore, MemoryRecordInput, MemoryContextResult } from "@agentvault/connect-mls-store";
3
+ export interface OpenClawFactsOptions {
4
+ workspaceDir?: string;
5
+ factsCharBudget?: number;
6
+ }
3
7
  /**
4
8
  * MemoryProvider for an OpenClaw-hosted agent (Cortina). context() merges
5
9
  * OpenClaw's own text history (read-only, plain session .jsonl) with the
@@ -12,9 +16,14 @@ export declare class OpenClawMemoryProvider implements MemoryProvider {
12
16
  private readonly store;
13
17
  private readonly baseDir;
14
18
  private readonly agent;
19
+ private readonly factsOpts?;
15
20
  private noSession;
16
21
  private readFailed;
17
- constructor(store: SqliteMlsStateStore, baseDir: string, agent: string);
22
+ private factsMissing;
23
+ private factsReadFailed;
24
+ private factsTruncated;
25
+ private factsChars;
26
+ constructor(store: SqliteMlsStateStore, baseDir: string, agent: string, factsOpts?: OpenClawFactsOptions | undefined);
18
27
  record(input: MemoryRecordInput): Promise<{
19
28
  seq: number;
20
29
  deduped: boolean;
@@ -0,0 +1,38 @@
1
+ /** Spec D2 constants — pinned so tests are deterministic. "Chars" = UTF-16 code units. */
2
+ export declare const DEFAULT_FACTS_BUDGET = 8000;
3
+ export declare const MEMORY_MD_CAP = 6000;
4
+ export declare const MIN_FRAGMENT = 200;
5
+ export declare const MAX_SOURCE_BYTES: number;
6
+ /**
7
+ * Resolve the agent's OpenClaw workspace dir (spec D4). Ladder, first hit wins:
8
+ * 1. explicit override (credential workspace_dir) — operator beats everything;
9
+ * 2. openclaw.json, resolved the way OpenClaw itself would:
10
+ * 2a. agents.list entry with id===agent and a NON-EMPTY string workspace (verbatim,
11
+ * never case-transformed — the live config declares "workspace-cortina" for an
12
+ * on-disk "workspace-Cortina" and works only because APFS is case-insensitive);
13
+ * 2b. else agents.defaults.workspace when it is a non-empty string (covers main,
14
+ * whose live entry has the workspace key ABSENT, and named agents w/o one);
15
+ * 3. derivation, LAST resort (openclaw.json missing/malformed/silent):
16
+ * main/"" → <base>/workspace, else <base>/workspace-<agent> verbatim.
17
+ * Empty agent is treated as "main" throughout (the factory passes agent ?? "").
18
+ * Total: always returns a path; existence is checked at read time, fail-safe.
19
+ */
20
+ export declare function resolveWorkspaceDir(baseDir: string, agent: string, override?: string): string;
21
+ export interface WorkspaceFactsResult {
22
+ /** 0..3 provenance-prefixed facts: MEMORY.md first, then dailies newest-first. */
23
+ facts: string[];
24
+ /** True when the budget clipped or dropped any source (includes the >1MB skip). */
25
+ truncated: boolean;
26
+ /** True when the resolved workspace yielded zero usable sources. False on budget-0 kill switch. */
27
+ missing: boolean;
28
+ }
29
+ /**
30
+ * Read the workspace facts (spec D1-D3). READ-ONLY, total: never throws for any
31
+ * filesystem state — absence degrades to fewer/zero facts. Only MEMORY.md and
32
+ * memory/\d{4}-\d{2}-\d{2}\.md under the resolved dir are ever opened.
33
+ * Budget accounting is content-only; prefixes and markers are exempt (audit M2).
34
+ */
35
+ export declare function readWorkspaceFacts(baseDir: string, agent: string, opts?: {
36
+ workspaceDir?: string;
37
+ charBudget?: number;
38
+ }): WorkspaceFactsResult;
@@ -1,4 +1,5 @@
1
1
  import type { SqliteMlsStateStore, MemoryRecordInput, MemoryContextResult } from "@agentvault/connect-mls-store";
2
+ import type { OpenClawFactsOptions } from "./openclaw-provider.js";
2
3
  export interface MemoryProvider {
3
4
  record(input: MemoryRecordInput): Promise<{
4
5
  seq: number;
@@ -26,4 +27,4 @@ export declare class NullMemory implements MemoryProvider {
26
27
  }>;
27
28
  context(): Promise<MemoryContextResult>;
28
29
  }
29
- export declare function createMemoryProvider(provider: "securecoms" | "none" | "hermes" | "openclaw", store: SqliteMlsStateStore, baseDir?: string, agent?: string): MemoryProvider;
30
+ export declare function createMemoryProvider(provider: "securecoms" | "none" | "hermes" | "openclaw", store: SqliteMlsStateStore, baseDir?: string, agent?: string, openclawFacts?: OpenClawFactsOptions): MemoryProvider;
@@ -1,3 +1,4 @@
1
+ import { type HermesConfigIoDeps, type Harness } from "./wire.js";
1
2
  export interface AutoWireInput {
2
3
  harness: string | undefined;
3
4
  identityId: string;
@@ -7,10 +8,10 @@ export interface AutoWireInput {
7
8
  tokenFilePath: string;
8
9
  connectorsSrcDir: string;
9
10
  }
10
- export interface AutoWireDeps {
11
+ export interface AutoWireDeps extends HermesConfigIoDeps {
11
12
  statDir: (p: string) => boolean;
12
13
  readBinding: (dstDir: string) => string | null;
13
- copyConnector: (srcDir: string, dstDir: string) => void;
14
+ copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
14
15
  writeBinding: (dstDir: string, identityId: string) => void;
15
16
  log: (s: string) => void;
16
17
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Strict, line-based editor for `~/.hermes/config.yaml`'s `plugins.enabled`
3
+ * block sequence. Hermes only loads user plugins listed there (plugins.py's
4
+ * loader — see spec §4), so `wire --harness hermes` must add our plugin's
5
+ * entry or the copied connector never runs.
6
+ *
7
+ * This module is PURE: it takes the file's text and returns either a noop
8
+ * (already correct / refused) or the edited text. File I/O and the
9
+ * `config.yaml.bak-wire-<ts>-<pid>` backup live with the caller (wire.ts /
10
+ * harness-wire.ts, Task 6).
11
+ *
12
+ * Hardened rules (spec §4, audited against the real production file at
13
+ * `config.yaml:569-573`):
14
+ * 1. The inserted item copies the EXISTING first item's exact column —
15
+ * never `key-indent + 2`. Production shape has items at the SAME
16
+ * indent as the `enabled:` key itself (a legal, if unusual, YAML block
17
+ * sequence style); some configs indent items by +2. Both must work,
18
+ * which is why the column is READ from the first item, never computed.
19
+ * 2. Insertion lands after the LAST consecutive same-column `- ` item —
20
+ * stop scanning at the first line that isn't one (the boundary before
21
+ * a sibling key such as `disabled: []`).
22
+ * 2b. Stopping the scan is not enough on its own: the line the scan
23
+ * stopped at must be a LEGITIMATE list terminator before the insertion
24
+ * is allowed to happen there. Legitimate means one of: (a) a sibling
25
+ * key at the same column as `enabled:` itself (e.g. `disabled:`), (b)
26
+ * end-of-block via blank-line(s) then a genuine dedent below the
27
+ * items' column, or (c) end of file. Anything else that stops the
28
+ * scan — a bare `-`, a mapping-style item (`- ` followed by an
29
+ * indented `key: value` on the next line), a blank line followed by
30
+ * MORE same-column items (ambiguous: did the writer mean to end the
31
+ * list or just add a spacer?), or any other unrecognized content —
32
+ * refuses rather than insert at a spot that was never verified to be
33
+ * the list's actual end.
34
+ * 3. `plugins.disabled` is checked too: an entry listed there wins over
35
+ * `plugins.enabled` in the loader, so refuse rather than add a second,
36
+ * shadowed entry.
37
+ * 4. Any structural surprise — no `plugins:` block, an inline
38
+ * `enabled: [...]` form, an empty enabled list, mixed-indent items, a
39
+ * comment line interleaved with items (ambiguous: it could be a
40
+ * trailing comment on the item above or a leading comment on the item
41
+ * below), or a non-boundary list terminator (see 2b) — refuses with no
42
+ * write and an actionable message. Never reformats, never touches any
43
+ * other key.
44
+ */
45
+ export declare const HERMES_PLUGIN_NAME = "securecoms-platform";
46
+ export interface EnsureHermesPluginEnabledResult {
47
+ action: "noop" | "edited";
48
+ text?: string;
49
+ reason?: string;
50
+ }
51
+ export declare function ensureHermesPluginEnabled(configText: string): EnsureHermesPluginEnabledResult;