@securecoms/agent-bridge 0.30.1 → 0.30.3

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.
@@ -46,6 +46,7 @@ import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail
46
46
  import { removeManagedLlmSource, writeManagedFiles, writeManifestPolicy } from "./service/runtime-config-dir.js";
47
47
  import { bindRuntimeConfig } from "./grants/binding.js";
48
48
  import type { RuntimeReplyReceipt } from "./reply-receipts.js";
49
+ import type { SearchProvider } from "./grants/search-providers.js";
49
50
  export type AgentAudience = "owner_dm" | "team_room" | "partner_room" | "other_human_dm" | "agent_to_agent" | "unknown";
50
51
  export type BridgeToolGrant =
51
52
  /** `source: "manifest"` (owner grants plan 4): issued from an owner-signed manifest; enforced only by a manifest_grants_v1 runtime. */
@@ -444,6 +445,14 @@ export interface BridgeAgentDeps {
444
445
  * see start(). Injected so tests do not wait a minute.
445
446
  */
446
447
  bootDrainWaitMs?: number;
448
+ /**
449
+ * B-4 (D35): the debounce before the runtime-config refetch that follows a search_providers_v2 capability change.
450
+ * The policy file is rewritten at once; this only bounds the refetch to one per burst. Test seam; production
451
+ * uses {@link SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS}.
452
+ */
453
+ searchCapabilityRefetchDebounceMs?: number;
454
+ /** B-4 fix round 1: the longest a burst may defer its refetch after its first change. Test seam. */
455
+ searchCapabilityRefetchMaxWaitMs?: number;
447
456
  /**
448
457
  * SEC-06 #18: the wait between in-process reply publish retries after a rate
449
458
  * limit. Defaults to an unref'd setTimeout. Injected so tests do not sleep.
@@ -785,6 +794,10 @@ export interface AgentActionAPI {
785
794
  export declare const BRIDGE_VERSION: string;
786
795
  /** start() waits this long for its boot drain before it stops blocking on it. */
787
796
  export declare const BOOT_DRAIN_WAIT_MS_DEFAULT = 60000;
797
+ /** B-4 (D35): the refetch after a search_providers_v2 change waits this long, so a reconnect burst (close, then hello) runs one. */
798
+ export declare const SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1000;
799
+ /** B-4 fix round 1: a flapping socket still refetches no later than this after the first change of a burst. */
800
+ export declare const SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 10000;
788
801
  export declare class BridgeAgent {
789
802
  private readonly cfg;
790
803
  private readonly transport;
@@ -1174,6 +1187,8 @@ export declare class BridgeAgent {
1174
1187
  private bootSweepTimer?;
1175
1188
  private readonly rng;
1176
1189
  private readonly bootDrainWaitMs;
1190
+ private readonly searchCapabilityRefetchDebounceMs;
1191
+ private readonly searchCapabilityRefetchMaxWaitMs;
1177
1192
  /** Resolved (and cleared) each time an ingest cycle settles: see start(). */
1178
1193
  private ingestSettledWaiters;
1179
1194
  /** The boot key-package publish, when start() had to defer it. Test seam. */
@@ -3299,6 +3314,51 @@ export declare class BridgeAgent {
3299
3314
  noteManifestConsumer(token: object): void;
3300
3315
  clearManifestConsumer(token: object): void;
3301
3316
  manifestConsumerAttached(): boolean;
3317
+ /**
3318
+ * D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
3319
+ * manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
3320
+ * none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
3321
+ */
3322
+ private readonly searchProvidersSockets;
3323
+ /**
3324
+ * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
3325
+ * always recomputes (final review I-1), even when it is non-capable.
3326
+ */
3327
+ private searchProvidersState;
3328
+ private searchCapabilityRefetchTimer;
3329
+ /** When the current burst's first change happened (valid while the timer is set). */
3330
+ private searchCapabilityBurstStart;
3331
+ /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
3332
+ private lastReportedSearchProviders;
3333
+ noteSearchProvidersSocket(token: object, declared: boolean): void;
3334
+ clearSearchProvidersSocket(token: object): void;
3335
+ /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
3336
+ searchProvidersCapable(): boolean;
3337
+ /**
3338
+ * D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
3339
+ * manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
3340
+ * per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
3341
+ */
3342
+ signedSearchProvider(): SearchProvider | undefined;
3343
+ /** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
3344
+ private searchProviderUnsupported;
3345
+ /**
3346
+ * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
3347
+ * switches and the new capability, then a debounced refetch (one per burst) only when needed.
3348
+ */
3349
+ private onSearchProvidersMaybeChanged;
3350
+ /**
3351
+ * A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
3352
+ * the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
3353
+ */
3354
+ private searchCapabilityRefetchNeeded;
3355
+ /**
3356
+ * D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
3357
+ * so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
3358
+ * lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
3359
+ * Content-free; a write failure is logged and the debounced refetch tries again.
3360
+ */
3361
+ private recomputeManifestPolicy;
3302
3362
  noteConsumerCapability(state: "ok" | "missing" | "harness_consumer"): void;
3303
3363
  /**
3304
3364
  * Harness re-rule (2026-10-12, audit r2 H-3): true exactly when this agent has a pin loaded and a good
package/dist/config.d.ts CHANGED
@@ -56,6 +56,19 @@ export interface BridgeConfig {
56
56
  ownerGrantPin?: OwnerGrantPinResult;
57
57
  }
58
58
  export declare const REQUIRED: readonly ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
59
+ export type ConfigErrorReason = "credential_not_found" | "credential_unreadable" | "credential_not_json" | "credential_missing_field" | "credential_voice_invalid";
60
+ /**
61
+ * A key-file load failure with a fixed reason code, set by the step that
62
+ * failed (spec 2026-10-15 §5.2). Callers map the code to a log field and a
63
+ * fixed human line; nothing reads `message`, which carries no path, no
64
+ * identifier and no upstream error text.
65
+ */
66
+ export declare class ConfigError extends Error {
67
+ readonly reason: ConfigErrorReason;
68
+ readonly field?: (typeof REQUIRED)[number] | undefined;
69
+ readonly source?: "file" | "env" | undefined;
70
+ constructor(reason: ConfigErrorReason, field?: (typeof REQUIRED)[number] | undefined, source?: "file" | "env" | undefined);
71
+ }
59
72
  export declare function loadConfig(opts: {
60
73
  credentialPath: string;
61
74
  storePath?: string;
@@ -1,4 +1,5 @@
1
1
  import type { BridgeMessage } from "./bridge-agent.js";
2
+ import type { SearchProvider } from "./grants/search-providers.js";
2
3
  /**
3
4
  * The /v1/ws hello feature a consumer sends to declare per-tool grant
4
5
  * enforcement. The runtime exports the same value (connect-agent-runtime
@@ -23,6 +24,8 @@ export declare const PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
23
24
  export declare function withConsumerGrants(m: BridgeMessage, perToolConsumer: boolean): BridgeMessage;
24
25
  /** The /v1/ws hello feature of a runtime that enforces manifest grants (runtime 0.13.0+). Pinned to the runtime's by capability-parity.test.ts. */
25
26
  export declare const MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
27
+ /** The /v1/ws hello feature of a runtime that supports the five signed search providers and web_extract (spec D3, D28). Pinned to the runtime's by capability-parity.test.ts. */
28
+ export declare const SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
26
29
  export declare const CONSTANT_TOOL_NAMES: Readonly<Record<"owner_dm" | "team_room", readonly string[]>>;
27
30
  /**
28
31
  * Spec §7.9: a consumer that did not declare manifest_grants_v1 (runtime 0.12.2, a harness,
@@ -40,3 +43,15 @@ export declare function withManifestCapability(m: BridgeMessage, manifestConsume
40
43
  * Never mutates `m`. Callers: the three harness delivery paths, after withConsumerGrants.
41
44
  */
42
45
  export declare function withHarnessGrants(m: BridgeMessage, withheld: boolean): BridgeMessage;
46
+ /**
47
+ * D19 item 3, D34, M-3, P-2 (B-6): the per-delivery search filter, applied on every delivery path AFTER
48
+ * withManifestCapability (and so after withHarnessGrants), judged by the consumer's OWN hello. A consumer that did not
49
+ * declare search_providers_v2 (an older runtime, a harness, every HTTP path) never receives:
50
+ * - a `web_extract` grant, whatever the signed provider (defence in depth beside the policy file, D19 item 1); and
51
+ * - a `web_search` grant, marked OR unmarked (the owner-DM constant and `constants ∩ signed` included), whenever the
52
+ * provider signed in the stored verified manifest is defined and not `brave`. `tavily` too (D34): an older runtime
53
+ * reads a readable install-time Brave key first, so only a signed `brave` cannot reach an unsigned vendor.
54
+ * `signedProvider` comes from the agent's stored verified manifest (BridgeAgent.signedSearchProvider), never the payload.
55
+ * Every other grant and field is unchanged. Returns `m` by reference when nothing is dropped; never mutates `m`.
56
+ */
57
+ export declare function withSearchCapability(m: BridgeMessage, consumerHasFeature: boolean, signedProvider: SearchProvider | undefined): BridgeMessage;
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The config binding and signed-target composer (owner-signed tool grants, spec §7.8, §7.10, §7.12;
3
+ * plan 4 Task 10). One pure function decides every file the bridge writes under a manifest.
4
+ *
5
+ * Rule: under a manifest the bridge writes a widening input only from a signed value, or from the
6
+ * payload when it equals the signed value; it never writes a mismatched widening input. Two ruled
7
+ * exceptions: a mismatched search block is replaced by the last applied good one (rulings item 3), or with none
8
+ * usable by the signed provider alone with no key file (D24 (i)),
9
+ * and llm-source.json is always the bridge's own render of the signed provider and model (item 5).
10
+ *
11
+ * Never throws (audit r1 H-4): every read of server data goes through `guarded`, and a failure is a
12
+ * mismatch of that field with nothing written for it.
13
+ */
14
+ import type { SearchProvider } from "./search-providers.js";
1
15
  import { type ToolGrantManifest } from "./manifest.js";
2
16
  import type { ManifestPolicyDoc } from "../service/runtime-config-dir.js";
3
17
  export declare const CONFIG_BINDING_FIELDS: readonly ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"];
@@ -14,7 +28,7 @@ export interface BindPayload {
14
28
  }
15
29
  /** The search block last written under a good search match for this pinned key (Task 11's sealed record); undefined = never. */
16
30
  export type AppliedSearch = {
17
- readonly provider: "brave" | "tavily";
31
+ readonly provider: SearchProvider;
18
32
  readonly keyFile?: string;
19
33
  } | null;
20
34
  export interface BindPrior {
@@ -50,9 +64,23 @@ export declare function legacyAssignToken(d: {
50
64
  fact_hash: string;
51
65
  channel_id: string;
52
66
  }): string;
67
+ /** The manifest's signed tools for one audience, inside the code ceiling; web_extract only with the capability (D19 item 1, D28). */
68
+ export declare function signedManifestTools(manifest: ToolGrantManifest, audience: "owner_dm" | "team_room", searchProvidersCapable: boolean): string[];
69
+ /**
70
+ * manifest-policy.json from the stored manifest, the kill switches and the capability alone (D35): the bridge
71
+ * recomputes it locally on a capability change, without a refetch or a cached BindResult. An audience switched off
72
+ * contributes no tool (review H-1).
73
+ */
74
+ export declare function manifestPolicyFor(manifest: ToolGrantManifest, switches: {
75
+ readonly ownerDm: boolean;
76
+ readonly room: boolean;
77
+ }, searchProvidersCapable: boolean): ManifestPolicyDoc;
53
78
  export declare function bindRuntimeConfig(manifest: ToolGrantManifest, payload: BindPayload, prior: BindPrior,
54
79
  /** Review H-1: the two manifest kill switches; an audience switched off contributes no tool to the policy file. */
55
80
  switches?: {
56
81
  readonly ownerDm: boolean;
57
82
  readonly room: boolean;
58
- }): BindResult;
83
+ },
84
+ /** D19 item 1, D28: every attached runtime socket declared search_providers_v2. Default false = fail closed:
85
+ * an older runtime reads an unknown tool in manifest-policy.json as an unreadable file and drops every grant. */
86
+ searchProvidersCapable?: boolean): BindResult;
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Owner-signed tool grants, step 1 (spec §7.1, §8.1): verify the owner-signed
3
+ * tool-grant manifest wrapper `{ manifest, algo, signature }`.
4
+ *
5
+ * A pure function: an untrusted payload value in, "verified" or one named
6
+ * refusal out. In step 1 a verified manifest is only RECORDED; no grant code
7
+ * reads it (Task 8 pins that).
8
+ */
9
+ import { type SearchProvider } from "./search-providers.js";
1
10
  import { type OwnerGrantPin } from "./pin.js";
2
11
  export declare const TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
3
12
  export declare const MANIFEST_STATES: readonly ["ok", "unchanged", "no_pin", "no_manifest", "malformed", "unsupported_algo", "bad_signature", "wrong_agent", "wrong_owner", "rollback", "unknown_entry", "kept_last_good"];
@@ -41,7 +50,8 @@ export interface SignedConfig {
41
50
  readonly model: string;
42
51
  };
43
52
  readonly search?: {
44
- readonly provider: "brave" | "tavily";
53
+ readonly provider: SearchProvider;
54
+ readonly owner_dm_daily_cap?: number;
45
55
  };
46
56
  readonly tools_allow?: readonly string[];
47
57
  readonly flags: readonly string[];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Search providers the bridge accepts in a signed manifest (spec 2026-10-16 5.1, D2, D36).
3
+ * The ONE bridge copy of the union; a parity test pins it against the runtime's SEARCH_PROVIDERS (ST-38).
4
+ */
5
+ export type SearchProvider = "brave" | "tavily" | "parallel" | "exa" | "firecrawl";
6
+ export declare const SEARCH_PROVIDERS: ReadonlySet<SearchProvider>;
7
+ /** Upper bound of the signed owner-DM daily search cap (D36, approved OQ-7). */
8
+ export declare const OWNER_DM_SEARCH_DAILY_CAP_MAX = 2000;
9
+ export declare function isSearchProvider(value: unknown): value is SearchProvider;
@@ -1,6 +1,7 @@
1
1
  import { type Server } from "node:http";
2
2
  import { z } from "zod";
3
3
  import type { BridgeAgent, BridgeMessage } from "./bridge-agent.js";
4
+ import type { SearchProvider } from "./grants/search-providers.js";
4
5
  /**
5
6
  * #335 handling decision: the throwing-write swallow stays (a dying socket
6
7
  * must cost only that connection's remainder — the caller requeues it and
@@ -26,6 +27,7 @@ export declare function deliverSseBatch(res: Parameters<typeof writeSse>[0], bat
26
27
  ackRuntimeMessages(ids: string[]): void;
27
28
  requeueFront(msgs: BridgeMessage[]): void;
28
29
  harnessGrantsWithheld(): boolean;
30
+ signedSearchProvider(): SearchProvider | undefined;
29
31
  }, onWriteError?: (error: unknown) => void): void;
30
32
  /**
31
33
  * Once-per-connection write-failure reporter (#335). A throwing `res.write`