@securecoms/agent-bridge 0.33.1 → 0.35.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.
@@ -45,7 +45,8 @@ import { type CallBufferTurn } from "./voice/call-buffer.js";
45
45
  import { type VaultListNode, type VaultReadExpectations } from "./vault.js";
46
46
  import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
47
47
  import { removeManagedLlmSource, writeManagedFiles, writeManifestPolicy } from "./service/runtime-config-dir.js";
48
- import { bindRuntimeConfig } from "./grants/binding.js";
48
+ import { type ReplyCredit } from "./reply-credit.js";
49
+ import { bindRuntimeConfig, type FeatureSet } from "./grants/binding.js";
49
50
  import type { RuntimeReplyReceipt } from "./reply-receipts.js";
50
51
  import type { SearchProvider } from "./grants/search-providers.js";
51
52
  export type AgentAudience = "owner_dm" | "team_room" | "partner_room" | "other_human_dm" | "agent_to_agent" | "unknown";
@@ -506,6 +507,13 @@ export interface BridgeAgentDeps {
506
507
  * tests to avoid real file IO / assert pin persistence directly.
507
508
  */
508
509
  vaultOwnerStore?: OwnerAuthorityStore;
510
+ /**
511
+ * Security fix 2026-10-30 (option A): the ONLY directory `/v1/vault-write`
512
+ * and `/v1/send-attachment` read from. Defaults to
513
+ * `join(dirname(cfg.storePath), "vault-outbox")`, or null (every upload
514
+ * refused) for an in-memory store. Override in tests.
515
+ */
516
+ vaultOutboxDir?: string | null;
509
517
  }
510
518
  /**
511
519
  * Voice repeats (a), owner report 2026-10-02: one spoken question reached the
@@ -692,6 +700,11 @@ export interface SendOptions {
692
700
  * enter the call buffer. `POST /v1/send`, SSE and harness replies never set it.
693
701
  */
694
702
  callBufferReply?: true;
703
+ /**
704
+ * Weather/currency Task 17 (OQ-P1): set only by the ws send handler from a frame whose credit is the closed value.
705
+ * The published text gets the U-25 suffix; history, unified memory and the call buffer get the text without it.
706
+ */
707
+ credit?: ReplyCredit;
695
708
  }
696
709
  /**
697
710
  * Task 4b (spec §2.2 A-6): a voice-scoped turn's history, read ONCE at ingest, the buffer seq it ends at and the
@@ -1514,6 +1527,9 @@ export declare class BridgeAgent {
1514
1527
  private readonly uploadBlobImpl;
1515
1528
  /** Track 5: directory where received attachments are saved. */
1516
1529
  private readonly attachmentsDirImpl;
1530
+ private readonly vaultOutboxDirImpl;
1531
+ /** Audit 2026-10-30 HIGH 2: the outbox pinned at start(); null = every upload refused. */
1532
+ private outboxPin;
1517
1533
  /** B7: persistence for the vault kekset's owner-pin TOFU state. */
1518
1534
  private readonly vaultOwnerStoreImpl;
1519
1535
  /** B7: last-seen kekset generation per enclave, enforced monotonic across
@@ -2047,7 +2063,12 @@ export declare class BridgeAgent {
2047
2063
  */
2048
2064
  private reclassifyForReply;
2049
2065
  private enforceBodyRateFloor;
2050
- /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
2066
+ /**
2067
+ * Bookkeeping after a CONFIRMED publish of `body` under `id`. `recordText` (weather/currency Task 17) is what a text
2068
+ * reply is RECORDED as: the published text minus any credit line. Every record site below reads `recorded`, never
2069
+ * `body.text`. Every production caller passes it (pinned by reply-credit.test.ts: text, or null for an a2a body);
2070
+ * undefined is left only for direct test calls of an uncredited body, where the published and record texts are equal.
2071
+ */
2051
2072
  private afterPublish;
2052
2073
  /**
2053
2074
  * Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
@@ -2103,6 +2124,12 @@ export declare class BridgeAgent {
2103
2124
  * scope and part, and its body matches its MAC (which binds id and channel).
2104
2125
  */
2105
2126
  private intentVerifies;
2127
+ /**
2128
+ * Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
2129
+ * for an a2a body). "weatherapi": the text minus the exact credit suffix. Undefined: the row is refused (an unknown
2130
+ * credit, a credited a2a body, or a credited text without the exact suffix).
2131
+ */
2132
+ private recordTextOf;
2106
2133
  /**
2107
2134
  * SEC-06 #18 item 3: a rate-limited reply retries the PUBLISH in-process
2108
2135
  * with backoff, never the model turn; so does an exact resend whose
@@ -2317,12 +2344,17 @@ export declare class BridgeAgent {
2317
2344
  * checks, so the HTTP boundary maps ALL vault trust failures to one code.
2318
2345
  */
2319
2346
  private getVaultSession;
2320
- /** Read `path` off local disk, encrypt + wrap its DEK under the vault's
2347
+ /** Read `path` from the vault outbox (security fix 2026-10-30; see
2348
+ * ./outbox.ts), encrypt + wrap its DEK under the vault's
2321
2349
  * current KEK generation, and create+upload+commit it as a new vault
2322
2350
  * file node named `name` (which may differ from `path`'s basename). */
2323
2351
  vaultWrite(enclaveId: string, vaultChannelId: string | null | undefined, path: string, name: string): Promise<{
2324
2352
  node_id: string;
2325
2353
  }>;
2354
+ /** Audit 2026-10-30 MED 5: an uploaded file leaves the outbox (no plaintext lingers). Content-free log. */
2355
+ private consumeUploaded;
2356
+ /** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
2357
+ vaultOutboxPath(): string | null;
2326
2358
  /** Download + verify + decrypt a vault file node. Returns the plaintext
2327
2359
  * base64-encoded (the loopback boundary is JSON-only, mirroring how
2328
2360
  * useVault.downloadFile returns `{name, bytes}` in-memory — here the
@@ -3504,17 +3536,31 @@ export declare class BridgeAgent {
3504
3536
  */
3505
3537
  private readonly searchProvidersSockets;
3506
3538
  /**
3507
- * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
3508
- * always recomputes (final review I-1), even when it is non-capable.
3539
+ * Weather/currency U6 (H-1): the same AND-over-sockets state for utility_tools_v1, kept separately per socket, so a
3540
+ * runtime that declares search_providers_v2 but not utility_tools_v1 (0.14.0, 0.14.1) keeps web_extract and never
3541
+ * sees a utility name.
3542
+ */
3543
+ private readonly utilityToolsSockets;
3544
+ /**
3545
+ * The (search, utility) tuple the files were last recomputed for; null until the first socket observation, so that
3546
+ * observation always recomputes (final review I-1), even when it is non-capable. One trigger keyed on BOTH values.
3509
3547
  */
3510
- private searchProvidersState;
3548
+ private capabilityState;
3511
3549
  private searchCapabilityRefetchTimer;
3512
3550
  /** When the current burst's first change happened (valid while the timer is set). */
3513
3551
  private searchCapabilityBurstStart;
3514
3552
  /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
3515
3553
  private lastReportedSearchProviders;
3554
+ /** U6: the utility_tools_v1 value in the last report posted (null until one is). */
3555
+ private lastReportedUtilityTools;
3516
3556
  noteSearchProvidersSocket(token: object, declared: boolean): void;
3517
3557
  clearSearchProvidersSocket(token: object): void;
3558
+ noteUtilityToolsSocket(token: object, declared: boolean): void;
3559
+ clearUtilityToolsSocket(token: object): void;
3560
+ /** U6: every attached runtime socket declared utility_tools_v1; no socket attached means absent (false). */
3561
+ utilityToolsCapable(): boolean;
3562
+ /** U6: the features every attached runtime socket declared, for the policy functions. */
3563
+ capabilityFeatures(): FeatureSet;
3518
3564
  /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
3519
3565
  searchProvidersCapable(): boolean;
3520
3566
  /**
@@ -3529,7 +3575,7 @@ export declare class BridgeAgent {
3529
3575
  * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
3530
3576
  * switches and the new capability, then a debounced refetch (one per burst) only when needed.
3531
3577
  */
3532
- private onSearchProvidersMaybeChanged;
3578
+ private onCapabilityMaybeChanged;
3533
3579
  /**
3534
3580
  * A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
3535
3581
  * the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
@@ -26,6 +26,8 @@ export declare function withConsumerGrants(m: BridgeMessage, perToolConsumer: bo
26
26
  export declare const MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
27
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
28
  export declare const SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
29
+ /** Weather/currency spec §8.1 (U6): the /v1/ws hello feature of a runtime that knows weather_forecast and currency_convert. Pinned to the runtime's by capability-parity.test.ts. */
30
+ export declare const UTILITY_TOOLS_FEATURE = "utility_tools_v1";
29
31
  export declare const CONSTANT_TOOL_NAMES: Readonly<Record<"owner_dm" | "team_room", readonly string[]>>;
30
32
  /**
31
33
  * Spec §7.9: a consumer that did not declare manifest_grants_v1 (runtime 0.12.2, a harness,
@@ -55,3 +57,10 @@ export declare function withHarnessGrants(m: BridgeMessage, withheld: boolean):
55
57
  * Every other grant and field is unchanged. Returns `m` by reference when nothing is dropped; never mutates `m`.
56
58
  */
57
59
  export declare function withSearchCapability(m: BridgeMessage, consumerHasFeature: boolean, signedProvider: SearchProvider | undefined): BridgeMessage;
60
+ /**
61
+ * Weather/currency spec §8.1 U6 item 3: the per-delivery utility filter, on every delivery path, judged by the
62
+ * consumer's OWN hello. A consumer that did not declare utility_tools_v1 (0.14.x and older runtimes, a harness, every
63
+ * HTTP path) never receives weather_forecast or currency_convert, marked or unmarked. Defence in depth beside the
64
+ * policy file and TOOL_FEATURE. Returns `m` by reference when nothing drops; never mutates `m`.
65
+ */
66
+ export declare function withUtilityCapability(m: BridgeMessage, consumerHasFeature: boolean): BridgeMessage;
package/dist/errors.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * WebSocket returns `{type:"error", error, message, detail?}`. A client gets the
7
7
  * same `error` code regardless of which transport it used. (A2A §9.5/§10.6 pattern.)
8
8
  */
9
- export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "agent_paused" | "send_forbidden" | "group_superseded" | "unknown_action_run" | "vault_integrity_failed" | "vault_ref_changed" | "session_superseded" | "internal";
9
+ export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "agent_paused" | "send_forbidden" | "group_superseded" | "unknown_action_run" | "vault_integrity_failed" | "vault_ref_changed" | "session_superseded" | "reply_credit_unknown" | "path_outside_outbox" | "internal";
10
10
  export interface BridgeErrorSpec {
11
11
  /** HTTP status this code maps to (the WS binding ignores it). */
12
12
  http: number;
@@ -64,8 +64,21 @@ export declare function legacyAssignToken(d: {
64
64
  fact_hash: string;
65
65
  channel_id: string;
66
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[];
67
+ /**
68
+ * Weather/currency spec §8.1 (U6, H-1): a runtime hello feature that a manifest tool needs. An older runtime reads an
69
+ * unknown name in manifest-policy.json as an unreadable file and drops EVERY manifest grant, so a tool listed here is
70
+ * written to the policy file (and delivered) only when every attached runtime socket declared its feature.
71
+ */
72
+ export type Feature = "search_providers_v2" | "utility_tools_v1";
73
+ export type FeatureSet = ReadonlySet<Feature>;
74
+ /** No socket attached, or none declared anything: fail closed. */
75
+ export declare const NO_FEATURES: FeatureSet;
76
+ /** The tool to feature table. A tool absent from it needs no feature. */
77
+ export declare const TOOL_FEATURE: Readonly<Record<string, Feature>>;
78
+ /** True when `tool` needs no feature, or its feature is in `features`. */
79
+ export declare function toolAllowedByFeatures(tool: string, features: FeatureSet): boolean;
80
+ /** The manifest's signed tools for one audience, inside the code ceiling; a TOOL_FEATURE tool only with its feature (D19 item 1, D28, U6). */
81
+ export declare function signedManifestTools(manifest: ToolGrantManifest, audience: "owner_dm" | "team_room", features: FeatureSet): string[];
69
82
  /**
70
83
  * manifest-policy.json from the stored manifest, the kill switches and the capability alone (D35): the bridge
71
84
  * recomputes it locally on a capability change, without a refetch or a cached BindResult. An audience switched off
@@ -74,13 +87,13 @@ export declare function signedManifestTools(manifest: ToolGrantManifest, audienc
74
87
  export declare function manifestPolicyFor(manifest: ToolGrantManifest, switches: {
75
88
  readonly ownerDm: boolean;
76
89
  readonly room: boolean;
77
- }, searchProvidersCapable: boolean): ManifestPolicyDoc;
90
+ }, features: FeatureSet): ManifestPolicyDoc;
78
91
  export declare function bindRuntimeConfig(manifest: ToolGrantManifest, payload: BindPayload, prior: BindPrior,
79
92
  /** Review H-1: the two manifest kill switches; an audience switched off contributes no tool to the policy file. */
80
93
  switches?: {
81
94
  readonly ownerDm: boolean;
82
95
  readonly room: boolean;
83
96
  },
84
- /** D19 item 1, D28: every attached runtime socket declared search_providers_v2. Default false = fail closed:
97
+ /** D19 item 1, D28, U6: the features every attached runtime socket declared. Default empty = fail closed:
85
98
  * an older runtime reads an unknown tool in manifest-policy.json as an unreadable file and drops every grant. */
86
- searchProvidersCapable?: boolean): BindResult;
99
+ features?: FeatureSet): BindResult;
@@ -58,47 +58,21 @@ export declare function capabilities(opts: {
58
58
  webhook_push: boolean;
59
59
  vault_ref: number;
60
60
  };
61
- export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
61
+ export declare const memoryRecordSchema: z.ZodObject<{
62
62
  conversation_id: z.ZodString;
63
- role: z.ZodEnum<["peer", "agent"]>;
63
+ role: z.ZodEnum<{
64
+ agent: "agent";
65
+ peer: "peer";
66
+ }>;
64
67
  peer_id: z.ZodOptional<z.ZodString>;
65
68
  text: z.ZodString;
66
- modality: z.ZodEnum<["text", "voice"]>;
69
+ modality: z.ZodEnum<{
70
+ text: "text";
71
+ voice: "voice";
72
+ }>;
67
73
  message_id: z.ZodOptional<z.ZodString>;
68
74
  idempotency_key: z.ZodOptional<z.ZodString>;
69
- }, "strip", z.ZodTypeAny, {
70
- text: string;
71
- role: "agent" | "peer";
72
- conversation_id: string;
73
- modality: "text" | "voice";
74
- message_id?: string | undefined;
75
- idempotency_key?: string | undefined;
76
- peer_id?: string | undefined;
77
- }, {
78
- text: string;
79
- role: "agent" | "peer";
80
- conversation_id: string;
81
- modality: "text" | "voice";
82
- message_id?: string | undefined;
83
- idempotency_key?: string | undefined;
84
- peer_id?: string | undefined;
85
- }>, {
86
- text: string;
87
- role: "agent" | "peer";
88
- conversation_id: string;
89
- modality: "text" | "voice";
90
- message_id?: string | undefined;
91
- idempotency_key?: string | undefined;
92
- peer_id?: string | undefined;
93
- }, {
94
- text: string;
95
- role: "agent" | "peer";
96
- conversation_id: string;
97
- modality: "text" | "voice";
98
- message_id?: string | undefined;
99
- idempotency_key?: string | undefined;
100
- peer_id?: string | undefined;
101
- }>;
75
+ }, z.core.$strip>;
102
76
  export declare function createHttpServer(agent: BridgeAgent, opts: HttpServerOptions): {
103
77
  server: Server;
104
78
  closeAllSseStreams: () => void;