@securecoms/agent-bridge 0.29.0 → 0.30.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.
@@ -43,11 +43,16 @@ import { type ChatMessage } from "./voice/fast-prompt.js";
43
43
  import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
44
44
  import { type VaultListNode, type VaultReadExpectations } from "./vault.js";
45
45
  import type { AgentActionNodeType, AgentActionNodeStatus, AgentActionNodeDetail } from "@agentvault/connect-core";
46
+ import { removeManagedLlmSource, writeManagedFiles, writeManifestPolicy } from "./service/runtime-config-dir.js";
47
+ import { bindRuntimeConfig } from "./grants/binding.js";
46
48
  import type { RuntimeReplyReceipt } from "./reply-receipts.js";
47
49
  export type AgentAudience = "owner_dm" | "team_room" | "partner_room" | "other_human_dm" | "agent_to_agent" | "unknown";
48
- export type BridgeToolGrant = {
50
+ export type BridgeToolGrant =
51
+ /** `source: "manifest"` (owner grants plan 4): issued from an owner-signed manifest; enforced only by a manifest_grants_v1 runtime. */
52
+ {
49
53
  readonly tool: string;
50
54
  readonly mode: "read";
55
+ readonly source?: "manifest";
51
56
  } | {
52
57
  readonly tool: string;
53
58
  readonly mode: "side_effect";
@@ -99,6 +104,8 @@ export declare const ROOM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
99
104
  * construction (daemon startup). Unset or exactly `on` keeps the grants;
100
105
  * any other value (`off`, `0`, `false`, empty) sends `[]` on every turn.
101
106
  */
107
+ /** Audit L-2: the owner replay prune runs at most once per this interval. */
108
+ export declare const OWNER_REPLAY_PRUNE_INTERVAL_MS: number;
102
109
  export declare function ownerDmDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
103
110
  /**
104
111
  * Kill switch for ROOM_DEFAULT_READ_GRANTS (plan OQ-3, RULED), read once at
@@ -107,6 +114,13 @@ export declare function ownerDmDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): bo
107
114
  * room turn. It never touches the owner DM.
108
115
  */
109
116
  export declare function roomDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
117
+ /**
118
+ * Owner-signed tool grants kill switch, owner DM (spec §7.4). Read once at startup; unset or exactly
119
+ * "on" keeps. Off gives the fallback (constants ∩ signed, plus current_time), never more.
120
+ */
121
+ export declare function ownerDmManifestGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
122
+ /** Owner-signed tool grants kill switch, internal rooms (spec §7.4). Same rule as the owner-DM switch. */
123
+ export declare function roomManifestGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
110
124
  export interface BridgeAuthorization {
111
125
  readonly version: 1;
112
126
  readonly audience: AgentAudience;
@@ -316,7 +330,16 @@ export declare const ROOM_VOICE_GUIDANCE_WITH_RESPONDER = "You are in a group vo
316
330
  * `voiceDefaultResponder` (never a body-supplied claim).
317
331
  */
318
332
  export declare function selectVoiceGuidance(isDm: boolean | undefined, voiceDefaultResponder: boolean | undefined): string;
333
+ /** Owner grants plan 4 Task 11: TEST SEAM for the refetch's writers and binder (call order, throws). Production omits it. */
334
+ export interface GrantWriters {
335
+ writeManifestPolicy?: typeof writeManifestPolicy;
336
+ writeManagedFiles?: typeof writeManagedFiles;
337
+ bindRuntimeConfig?: typeof bindRuntimeConfig;
338
+ removeManagedLlmSource?: typeof removeManagedLlmSource;
339
+ }
319
340
  export interface BridgeAgentDeps {
341
+ /** Owner grants plan 4 Task 11: TEST SEAM only (see GrantWriters). */
342
+ grantWriters?: GrantWriters;
320
343
  /**
321
344
  * Unified memory part 1 (plan Task 8): TEST SEAM ONLY. Production (bin.ts) never passes it, so the compiled
322
345
  * UNIFIED_MEMORY_PART1 decides (switch.arch.test.ts pins both).
@@ -506,6 +529,7 @@ export declare function redeliveryDelayMs(attempts: number): number;
506
529
  * messages is dead-lettered. Fixed text — it never echoes the message.
507
530
  */
508
531
  export declare const DEAD_LETTER_NOTICE = "I couldn't process a message. Please resend it.";
532
+ export declare const ROOM_SEARCH_CAP_NOTICE = "I've used today's web searches for this room. I can still help without searching. Searches are available again tomorrow (UTC).";
509
533
  /** At most one {@link DEAD_LETTER_NOTICE} per channel per this window. */
510
534
  export declare const DEAD_LETTER_NOTICE_INTERVAL_MS: number;
511
535
  /**
@@ -758,6 +782,30 @@ export declare class BridgeAgent {
758
782
  private readonly fetchImpl;
759
783
  /** Round 5: home for the managed runtime-config directory (test seam). */
760
784
  private readonly home;
785
+ private readonly grantWriters;
786
+ /** Owner grants (spec §7.5, OQ-2): the operator cap, read once; the same variable the runtime reads. 0 = no room search. */
787
+ private readonly roomSearchDailyCap;
788
+ /** Review M-3 (ERIC APPROVALS 3): the per-agent total across rooms, read once; the same variable the runtime reads. */
789
+ private readonly agentRoomSearchDailyCap;
790
+ /** `${day}\n${channel}` keys already logged reached today (in memory; the store holds the durable flag). */
791
+ private readonly roomCapLogged;
792
+ /** Rooms whose K-14 send is in flight: a second frame posts nothing more. */
793
+ private readonly roomCapNoticePending;
794
+ /** Held entries whose staged copy lost web_search to the backstop; applied again at release. */
795
+ private readonly backstopStripped;
796
+ /**
797
+ * Owner grants plan 4 Task 11 (P-4, P-6): what the managed directory holds under the current manifest.
798
+ * Manifest grants require state "bound" at the applied counter (Task 13). null at startup, while a
799
+ * refetch narrows it, and after any throw (the fallback).
800
+ */
801
+ private manifestBinding;
802
+ /** skills.json tools.deny from the latest fetch, manifest or not (spec §5.2 `disabled`). */
803
+ private payloadToolDeny;
804
+ /** Spec §7.7 (OQ-5): the latest fetch's binding outcome, reported in tool_grant_status (Task 15). */
805
+ private configBindingMismatches;
806
+ private targetMismatches;
807
+ /** P-5 (Task 14): the legacy fact decisions the latest bound fetch may apply (assigns verified against the signed set). */
808
+ private boundLegacyDecisions;
761
809
  private readonly joinState;
762
810
  /**
763
811
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -1057,11 +1105,27 @@ export declare class BridgeAgent {
1057
1105
  private readonly ownerDmDefaultGrants;
1058
1106
  /** Read once at startup; see roomDefaultGrantsEnabled. */
1059
1107
  private readonly roomDefaultGrants;
1108
+ /** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
1109
+ private readonly ownerDmManifestGrants;
1110
+ private readonly roomManifestGrants;
1060
1111
  /**
1061
1112
  * Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
1062
1113
  * cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
1063
1114
  */
1064
1115
  private readonly ownerDmCycleFresh;
1116
+ private toolGrantManifest;
1117
+ private toolGrantCounter;
1118
+ private manifestState;
1119
+ /** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
1120
+ private ownerVerifyFailures;
1121
+ /** Verified owner turns of this ingest cycle, recorded in owner_replay when staged (P-3). */
1122
+ private readonly ownerReplayPending;
1123
+ /** D-9: ids of owner turns that failed verification (never `replay`, audit E-3), id -> channel. Persisted too. */
1124
+ private readonly ownerVerifyFailedIds;
1125
+ private serverOwnerMismatchLogged;
1126
+ /** Audit L-2: the replay prune runs at most once per interval, on a monotonic clock. */
1127
+ private monoNow;
1128
+ private lastOwnerReplayPruneAt;
1065
1129
  /**
1066
1130
  * Audit M-B1: room-granted turns, keyed by the inbound message id, with the
1067
1131
  * channel, MLS group and epoch the room was classified at (review I-1: an
@@ -1400,6 +1464,20 @@ export declare class BridgeAgent {
1400
1464
  */
1401
1465
  refetchRuntimeConfig(): Promise<void>;
1402
1466
  private refetchRuntimeConfigOnce;
1467
+ /**
1468
+ * Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
1469
+ * after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
1470
+ * the binding left null.
1471
+ */
1472
+ private applyUnderManifest;
1473
+ /** Spec §7.6 (plan 4 Task 15): content-free. One line per mismatched field, one per dropped target. */
1474
+ private logConfigBinding;
1475
+ /** H-4: the binding could not be computed. The memory still applies, with the signed mode; nothing else is written. */
1476
+ private bindFailed;
1477
+ /** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
1478
+ private overwriteStalePolicy;
1479
+ /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
1480
+ private postRuntimeConfigReport;
1403
1481
  /**
1404
1482
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
1405
1483
  *
@@ -2079,6 +2157,11 @@ export declare class BridgeAgent {
2079
2157
  * stale cache is refreshed once; any failure remains a fail-closed denial.
2080
2158
  */
2081
2159
  private mayReadOpenClawMemory;
2160
+ /**
2161
+ * Decision D-9 (audit H-3): a turn that failed owner verification gets only
2162
+ * its own text, never the prior-turn history snapshot, for every provider.
2163
+ */
2164
+ private mayReadHistoryForTurn;
2082
2165
  /** An inbound turn may receive OpenClaw memory only from an active owner DM. */
2083
2166
  private mayReadOpenClawMemoryForTurn;
2084
2167
  /**
@@ -2087,6 +2170,49 @@ export declare class BridgeAgent {
2087
2170
  * never falls back to the runtime's retired plaintext JSON files.
2088
2171
  */
2089
2172
  private memoryForTurn;
2173
+ /**
2174
+ * Owner grants plan 4 Task 16 (T-30, audit r2 H-1): the voice fence is on whenever a pin is loaded, on EVERY runtime
2175
+ * turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
2176
+ */
2177
+ private voiceFenceOn;
2178
+ /** The keyed and legacy fast-reply ids of every peer row of the conversation (audit r2 M-1; never the 50-row window). */
2179
+ private fenceContext;
2180
+ /**
2181
+ * Drop agent lines classified fast_path, untrusted or unknown from a turn's memory (every turn of a pinned agent).
2182
+ * `strict` (review H-3, a turn that holds a manifest grant): relay-class peer lines go too (sender not the pinned
2183
+ * owner, or voice modality). Otherwise peer lines are never dropped.
2184
+ */
2185
+ private fenceMemory;
2186
+ /**
2187
+ * Review H-3: true when a delivered frame holds a line a manifest turn would exclude (a relay-class peer line; agent
2188
+ * lines in a frame are already fenced). Such a turn's reply is recorded "untrusted".
2189
+ */
2190
+ private frameHoldsRelayClass;
2191
+ /**
2192
+ * Review H-3 attribution: message ids of turns delivered on a pinned agent whose frame held no relay-class line.
2193
+ * A reply is recorded "reply" only when its in-reply-to is here; anything else (tainted, unknown after a restart,
2194
+ * no in-reply-to) is "untrusted". Bounded; the oldest are forgotten first (forgetting only makes a reply untrusted).
2195
+ */
2196
+ private readonly cleanTurns;
2197
+ private noteDeliveredFrame;
2198
+ private readonly taintedTurns;
2199
+ /**
2200
+ * Review R2-H2: on a pinned agent a 7a note or a unified memory_note is written only from a turn delivered with a clean
2201
+ * frame in the owner DM (cleanTurns, not taintedTurns), so a tainted fallback turn cannot plant a note that a later
2202
+ * manifest turn reads. Taint is noted at delivery, before any note frame for the turn can arrive.
2203
+ */
2204
+ private noteTurnClean;
2205
+ /**
2206
+ * Review R2-H1: the turn itself is relay-class (its frame memory is prior-turn only, so its own sender is never in it):
2207
+ * a sender that is not the pinned owner, a relay speaker claim, or a voice turn.
2208
+ */
2209
+ private turnIsRelayClass;
2210
+ /**
2211
+ * R2-M1 ruling: the channel where the H-3 taint and the `untrusted` exclusion apply, on a pinned agent: the signed
2212
+ * owner_dm_channel_id of the manifest in force. Before any manifest is held there is no signed DM, so every channel
2213
+ * not known to be a room counts (fail closed: a reply recorded there before the first manifest must not reach it).
2214
+ */
2215
+ private taintScope;
2090
2216
  /**
2091
2217
  * Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
2092
2218
  * value turns notes off and deletes nothing. A readable one goes through
@@ -2630,6 +2756,41 @@ export declare class BridgeAgent {
2630
2756
  * cycle. Fail-soft inside refreshChannelRoster (never blocks ingest).
2631
2757
  */
2632
2758
  private refreshRostersForCycle;
2759
+ /**
2760
+ * Owner-signed tool grants, step 1 (spec §7.2; decisions D-1, D-2, D-4, D-8,
2761
+ * D-9; audits H-1, M-2, M-4, E-1..E-3). The verdict for one delivered turn.
2762
+ *
2763
+ * `unchecked` when no pin is loaded or the sender is not the pinned owner.
2764
+ * Otherwise, in order, the first failure wins: the identity signature over the
2765
+ * raw payload bytes with the pinned key; the verified bytes must be the
2766
+ * decrypted envelope; in a DM only (D-1), the signed DM channel and the MLS
2767
+ * leaf set; then the replay set and closed epochs, keyed by the agent's OWN
2768
+ * held group (never the backend's row label, M-2). NEVER throws (M-4): any
2769
+ * exception is `failed verify_error`, and the row is acked like any other turn.
2770
+ */
2771
+ private verifyOwnerTurn;
2772
+ /** Log, count and (except a replay, audit E-3) record one failed owner-turn check. Never throws (audit E-2). */
2773
+ private ownerTurnFailed;
2774
+ /** Review M-A: a verified original of this id was staged and is still pending, or was recorded in the replay set. */
2775
+ private isVerifiedOwnerOriginal;
2776
+ /** Review M-A: a verified original of this id was staged and acked (it is in the replay set, no longer pending). */
2777
+ private isAckedOwnerOriginal;
2778
+ /** D-9: memory first, then the store (a restart before the reply). Unpinned: never. A store read that throws withholds (fail closed). */
2779
+ private isOwnerVerifyFailed;
2780
+ /**
2781
+ * Decision D-2, audit L-2: prune the owner replay set at most once per
2782
+ * OWNER_REPLAY_PRUNE_INTERVAL_MS, keeping each channel's current (group,
2783
+ * epoch). A channel whose held position is unknown is left out (not pruned).
2784
+ */
2785
+ private maybePruneOwnerReplay;
2786
+ /** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
2787
+ private loadToolGrantState;
2788
+ /**
2789
+ * Spec §7.1: verify and record a served manifest. Never writes a file and
2790
+ * never changes a grant in step 1. A save that throws keeps the old state.
2791
+ */
2792
+ private applyToolGrantManifest;
2793
+ private logManifest;
2633
2794
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
2634
2795
  private authorizeTurn;
2635
2796
  /**
@@ -2693,6 +2854,8 @@ export declare class BridgeAgent {
2693
2854
  * enforcement is attached
2694
2855
  * memory_write the turn carries memory_write (Markdown memory
2695
2856
  * 7a, spec §8.3): a boolean, never content
2857
+ * grant_source where the grants came from: constant | none in
2858
+ * step 1 (owner-signed tool grants §7.6)
2696
2859
  */
2697
2860
  private logAuthzDecision;
2698
2861
  /**
@@ -2860,7 +3023,19 @@ export declare class BridgeAgent {
2860
3023
  /** Notify-only SSE signal: fired after a buffer push. Listener pulls via takeBuffered. */
2861
3024
  subscribe(listener: () => void): () => void;
2862
3025
  /** Synchronous splice of buffered messages (all, or one channel). Shared by drain + SSE. */
3026
+ /** Review H-1: every buffer exit narrows manifest grants again to the state at delivery. */
2863
3027
  takeBuffered(channelId?: string): BridgeMessage[];
3028
+ takeDeliverable(busyChannels: ReadonlySet<string>): BridgeMessage[];
3029
+ /** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
3030
+ private delivering;
3031
+ /**
3032
+ * Review H-1 (kill switches reach staged and restored turns): a grant marked source "manifest" survives delivery
3033
+ * only while its audience's manifest switch is on, the binding is bound at the applied counter, and the current
3034
+ * manifest still signs the tool. Otherwise it becomes the fallback: kept unmarked if it is a constant of the
3035
+ * audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
3036
+ */
3037
+ private renarrowManifestGrants;
3038
+ private takeBufferedRaw;
2864
3039
  /**
2865
3040
  * Phase 3 F2: per-channel ordered delivery for the /v1/ws runtime consumer.
2866
3041
  * Splices out, oldest first, at most ONE buffered message per channel that
@@ -2870,7 +3045,7 @@ export declare class BridgeAgent {
2870
3045
  * working on — which it used to refuse with a "busy" reply and ack — and a
2871
3046
  * backlog replays one message per channel at a time after a restart.
2872
3047
  */
2873
- takeDeliverable(busyChannels: ReadonlySet<string>): BridgeMessage[];
3048
+ private takeDeliverableRaw;
2874
3049
  private hasReplyIntent;
2875
3050
  /**
2876
3051
  * SEC-06 #18: answer a redelivered message from its reply intent. Counts as
@@ -2911,6 +3086,20 @@ export declare class BridgeAgent {
2911
3086
  * at most once per channel per hour. Logs carry counts only.
2912
3087
  */
2913
3088
  private deadLetter;
3089
+ /** Spec §7.5: the room is capped today: cap 0, reached (by the runtime or the backstop), or at the cap. Fails closed. */
3090
+ private roomSearchCapped;
3091
+ /** Spec §7.5 (T-14c, M-4): the runtime reports an executed-call cap for a room. Never throws. */
3092
+ noteRoomCapReached(channelId: string): void;
3093
+ /** K-14, once per room per UTC day; the flag is persisted only after the send resolves. Called only from noteRoomCapReached. */
3094
+ private noticeRoomSearchCap;
3095
+ private logRoomCap;
3096
+ /**
3097
+ * Spec §7.5 (T-14b, audit r1 H-3): the silent bridge backstop. Counts a team_room turn that actually goes out
3098
+ * to the runtime with web_search, once per message id (callers run it after their runtimePendingIds early
3099
+ * return). Past the cap, and on a store failure (fail closed), the entry loses web_search. Never posts K-14.
3100
+ */
3101
+ private applyRoomSearchBackstop;
3102
+ private withoutWebSearch;
2914
3103
  private noticeDeadLetter;
2915
3104
  /** Poke consumers once the earliest redelivery backoff ends. */
2916
3105
  private armRedeliveryWake;
@@ -3056,6 +3245,24 @@ export declare class BridgeAgent {
3056
3245
  /** Forget `token` when its socket closes; a newer consumer's token is kept. */
3057
3246
  clearPerToolConsumer(token: object): void;
3058
3247
  perToolConsumerAttached(): boolean;
3248
+ /**
3249
+ * Owner grants (spec §7.9, plan 4 Task 5): the attached per-tool runtime socket whose hello
3250
+ * declared manifest_grants_v1, or null. A manifest grant is issued only while one is attached
3251
+ * (Task 13); every other socket receives manifest grants narrowed to the constants.
3252
+ * Newest wins, like perToolConsumer.
3253
+ */
3254
+ private manifestConsumer;
3255
+ /** OQ-5: the latest hello's class; "missing" until any hello. Reported in tool_grant_status (Task 15). */
3256
+ private lastConsumerCapability;
3257
+ noteManifestConsumer(token: object): void;
3258
+ clearManifestConsumer(token: object): void;
3259
+ manifestConsumerAttached(): boolean;
3260
+ noteConsumerCapability(state: "ok" | "missing" | "harness_consumer"): void;
3261
+ /**
3262
+ * Harness re-rule (2026-10-12, audit r2 H-3): true exactly when this agent has a pin loaded and a good
3263
+ * manifest. A harness consumer then receives [] in every audience (consumer-grants.ts withHarnessGrants).
3264
+ */
3265
+ harnessGrantsWithheld(): boolean;
3059
3266
  health(): {
3060
3267
  status: "ok";
3061
3268
  identity_id: string;
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type VoiceConfig } from "./voice/persona.js";
2
+ import { type OwnerGrantPinResult } from "./grants/pin.js";
2
3
  import { type FastPathMode } from "./voice/defer-intent.js";
3
4
  export interface BridgeConfig {
4
5
  apiKey: string;
@@ -47,6 +48,12 @@ export interface BridgeConfig {
47
48
  modelSource: "download" | "import_only";
48
49
  /** Spec section 13 (R-U20): credential `memory.enabled`. Absent or true is on; any other value is off (plan I-9). Local only: the backend cannot set it. */
49
50
  memoryEnabled?: boolean;
51
+ /**
52
+ * Owner-signed tool grants (spec §6.1, §6.5): the grant pin, loaded once here
53
+ * from the credential's `owner_signing_key` and `owner-signing-key.json` beside
54
+ * the credential, never from the backend. Absent means `no_pin`.
55
+ */
56
+ ownerGrantPin?: OwnerGrantPinResult;
50
57
  }
51
58
  export declare const REQUIRED: readonly ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
52
59
  export declare function loadConfig(opts: {
@@ -21,3 +21,22 @@ export declare const PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
21
21
  * no non-per-tool consumer ever receives (Markdown memory 7a, review m-3).
22
22
  */
23
23
  export declare function withConsumerGrants(m: BridgeMessage, perToolConsumer: boolean): BridgeMessage;
24
+ /** 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
+ export declare const MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
26
+ export declare const CONSTANT_TOOL_NAMES: Readonly<Record<"owner_dm" | "team_room", readonly string[]>>;
27
+ /**
28
+ * Spec §7.9: a consumer that did not declare manifest_grants_v1 (runtime 0.12.2, a harness,
29
+ * every HTTP path) never receives a manifest grant. Its grants are narrowed to constants(a):
30
+ * a manifest-marked tool that is also a constant is kept unmarked; any other is dropped.
31
+ * Never mutates `m`.
32
+ */
33
+ export declare function withManifestCapability(m: BridgeMessage, manifestConsumer: boolean): BridgeMessage;
34
+ /**
35
+ * Harness re-rule (2026-10-12, supersedes P4 OQ-8's "constants ∩ signed"), audit r2 H-3. A harness turns
36
+ * ANY non-empty tool_grants into the owner's whole configured toolset (header of this file), so no
37
+ * non-empty list can stay within what the owner signed. On a pinned agent with a good manifest
38
+ * (`withheld === true`) a harness receives [] in EVERY audience. `withheld === false` (no pin, or no good
39
+ * manifest): `m` unchanged, by reference (today's behaviour). An issued [] is returned by reference.
40
+ * Never mutates `m`. Callers: the three harness delivery paths, after withConsumerGrants.
41
+ */
42
+ export declare function withHarnessGrants(m: BridgeMessage, withheld: boolean): BridgeMessage;
@@ -0,0 +1,58 @@
1
+ import { type ToolGrantManifest } from "./manifest.js";
2
+ import type { ManifestPolicyDoc } from "../service/runtime-config-dir.js";
3
+ export declare const CONFIG_BINDING_FIELDS: readonly ["persona", "memory", "search", "tools_allow", "flags", "llm", "files", "legacy_assign"];
4
+ export type ConfigBindingField = (typeof CONFIG_BINDING_FIELDS)[number];
5
+ export interface BindPayload {
6
+ readonly version?: number;
7
+ readonly files?: Record<string, string>;
8
+ readonly skills_json?: Record<string, unknown>;
9
+ readonly memory?: unknown;
10
+ readonly persona?: unknown;
11
+ /** Raw, unfiltered saved tokens (runtime_config_service.py memory_prunes). Untrusted. */
12
+ readonly memory_prunes?: unknown;
13
+ readonly legacy_memory_fact_decisions?: unknown;
14
+ }
15
+ /** The search block last written under a good search match for this pinned key (Task 11's sealed record); undefined = never. */
16
+ export type AppliedSearch = {
17
+ readonly provider: "brave" | "tavily";
18
+ readonly keyFile?: string;
19
+ } | null;
20
+ export interface BindPrior {
21
+ readonly search: AppliedSearch | undefined;
22
+ }
23
+ export interface BindResult {
24
+ readonly mismatches: readonly ConfigBindingField[];
25
+ readonly targetMismatches: {
26
+ readonly endpoint: number;
27
+ readonly database: number;
28
+ };
29
+ readonly files: Record<string, string>;
30
+ readonly memory: unknown;
31
+ readonly signedTargets: {
32
+ readonly endpoints: number;
33
+ readonly databases: number;
34
+ };
35
+ readonly policy: ManifestPolicyDoc;
36
+ /** Set only when `search` matched: the block written, to be saved as the new last good one after the write succeeds. */
37
+ readonly appliedSearch: AppliedSearch | undefined;
38
+ /**
39
+ * P-5 (Task 14): the decisions to hand applyLegacyMemoryFactDecisions. On a legacy_assign match: the
40
+ * deletes plus each assign whose token, rebuilt from its own fields, is in the verified saved set. On a
41
+ * mismatch: the deletes only.
42
+ */
43
+ readonly legacyDecisions: unknown[];
44
+ }
45
+ /** P4 OQ-7: hex SHA-256 of JCS(sorted, deduplicated memory_prunes entries that begin "legacy-fact-assign:"). Non-strings are ignored. */
46
+ export declare function legacyAssignSha256(prunes: readonly unknown[]): string;
47
+ /** The backend's assign token (runtime_config_service.py _legacy_memory_fact_decision_token), rebuilt from the fields. */
48
+ export declare function legacyAssignToken(d: {
49
+ source_id: string;
50
+ fact_hash: string;
51
+ channel_id: string;
52
+ }): string;
53
+ export declare function bindRuntimeConfig(manifest: ToolGrantManifest, payload: BindPayload, prior: BindPrior,
54
+ /** Review H-1: the two manifest kill switches; an audience switched off contributes no tool to the policy file. */
55
+ switches?: {
56
+ readonly ownerDm: boolean;
57
+ readonly room: boolean;
58
+ }): BindResult;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The effective grant (owner-signed tool grants, spec §5.2, §5.4, §7.4; plan 4 Task 6).
3
+ * The one formula, pure, so every rule has its own test and the issuing code decides nothing.
4
+ *
5
+ * signed(a) = manifest.grants[a] ∩ TOOL_GRANT_CODE_CEILING[a]
6
+ * effective(a) = signed(a) ∖ disabled, minus http_request with no signed endpoint, minus sql_query
7
+ * with no signed database, minus team_room web_search when capped, ∪ {current_time}
8
+ * (only when every precondition holds: `eligible` and the manifest switch on)
9
+ * fallback(a) = (constants(a) ∩ signed(a)) ∪ {current_time} when a precondition fails
10
+ * = constants(a) by reference with no good manifest
11
+ * [] when today's predicate gives []
12
+ */
13
+ import { type ToolGrantManifest } from "./manifest.js";
14
+ /** Spec §5.4 code ceiling. The same frozen lists Plan 1 uses to drop unknown entries. */
15
+ export declare const TOOL_GRANT_CODE_CEILING: {
16
+ readonly owner_dm: readonly string[];
17
+ readonly team_room: readonly string[];
18
+ };
19
+ export type GrantAudience = "owner_dm" | "team_room";
20
+ export type GrantSource = "constant" | "manifest" | "fallback" | "none";
21
+ export interface ReadGrant {
22
+ readonly tool: string;
23
+ readonly mode: "read";
24
+ readonly source?: "manifest";
25
+ }
26
+ export interface EffectiveInput {
27
+ readonly audience: GrantAudience;
28
+ /** Today's list from the existing predicate: OWNER_DM_DEFAULT_READ_GRANTS, ROOM_DEFAULT_READ_GRANTS, or a frozen []. */
29
+ readonly base: readonly ReadGrant[];
30
+ /** The last good manifest, or null (no pin, or none verified). */
31
+ readonly manifest: ToolGrantManifest | null;
32
+ /** Every manifest-only precondition held (Task 13 computes it). */
33
+ readonly eligible: boolean;
34
+ readonly manifestSwitchOn: boolean;
35
+ readonly disabled: ReadonlySet<string>;
36
+ readonly signedTargets: {
37
+ readonly endpoints: number;
38
+ readonly databases: number;
39
+ };
40
+ readonly roomSearchCapped: boolean;
41
+ }
42
+ export interface EffectiveGrant {
43
+ readonly grants: readonly ReadGrant[];
44
+ readonly source: GrantSource;
45
+ readonly withheldBySearchCap: boolean;
46
+ }
47
+ export declare function effectiveToolGrants(i: EffectiveInput): EffectiveGrant;
@@ -0,0 +1,68 @@
1
+ import { type OwnerGrantPin } from "./pin.js";
2
+ export declare const TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
3
+ 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"];
4
+ export type ManifestState = (typeof MANIFEST_STATES)[number];
5
+ /** Known names only for dropping unknown entries (§7.1 step 6). These are the §5.4 code-ceiling names; step 4 uses them as the ceiling. */
6
+ export declare const MANIFEST_KNOWN_TOOLS: {
7
+ readonly owner_dm: readonly string[];
8
+ readonly team_room: readonly string[];
9
+ };
10
+ export interface ToolGrantManifest {
11
+ readonly type: "securecoms.tool-grants";
12
+ readonly v: 1;
13
+ readonly agent_identity_id: string;
14
+ readonly owner_identity_id: string;
15
+ readonly owner_dm_channel_id?: string;
16
+ readonly owner_dm_relay_identity_id?: string;
17
+ readonly counter: number;
18
+ readonly grants: {
19
+ readonly owner_dm?: readonly string[];
20
+ readonly team_room?: readonly string[];
21
+ };
22
+ /** Plan 4 Task 7 (spec §8.1): signed targets. Absent in the wrapper = {}. A `headersFile: ""` is dropped (L-4). */
23
+ readonly endpoints: Readonly<Record<string, SignedEndpoint>>;
24
+ readonly databases: Readonly<Record<string, SignedDatabase>>;
25
+ /** Plan 4 Task 7 (spec §8.1, §7.10): the signed values the config binding compares. Required. */
26
+ readonly config: SignedConfig;
27
+ }
28
+ export interface SignedEndpoint {
29
+ readonly baseUrl: string;
30
+ readonly headersFile?: string;
31
+ }
32
+ export type SignedDatabase = {
33
+ readonly sqlite: string;
34
+ } | {
35
+ readonly postgresUrlFile: string;
36
+ };
37
+ export interface SignedConfig {
38
+ readonly persona_sha256: string;
39
+ readonly llm?: {
40
+ readonly provider: string;
41
+ readonly model: string;
42
+ };
43
+ readonly search?: {
44
+ readonly provider: "brave" | "tavily";
45
+ };
46
+ readonly tools_allow?: readonly string[];
47
+ readonly flags: readonly string[];
48
+ readonly memory: {
49
+ readonly mode: "markdown" | "semantic" | "off";
50
+ readonly recall_days?: 30 | 90 | 365;
51
+ };
52
+ readonly legacy_assign_sha256: string;
53
+ }
54
+ export type ManifestVerdict = {
55
+ state: "ok";
56
+ manifest: ToolGrantManifest;
57
+ droppedUnknown: boolean;
58
+ wrapper: unknown;
59
+ } | {
60
+ state: "unchanged" | "malformed" | "unsupported_algo" | "bad_signature" | "wrong_agent" | "wrong_owner" | "rollback";
61
+ };
62
+ /** utf8(TOOL_GRANTS_DOMAIN) || canonicalize(manifest). The domain keeps an envelope signature from ever verifying as a manifest. */
63
+ export declare function manifestSignedBytes(manifest: unknown): Uint8Array;
64
+ export declare function verifyToolGrantManifest(raw: unknown, ctx: {
65
+ pin: OwnerGrantPin;
66
+ agentIdentityId: string;
67
+ storedCounter: number | null;
68
+ }): ManifestVerdict;
@@ -0,0 +1,52 @@
1
+ import type { AgentVaultEnvelope } from "@agentvault/connect-core";
2
+ import { type OwnerGrantPin } from "./pin.js";
3
+ export declare const OWNER_VERIFY_STATES: readonly ["ok", "bad_signature", "malformed", "unknown_algo", "no_signature", "not_owner_dm_leaves", "wrong_dm_channel", "replay", "verify_error"];
4
+ export type OwnerVerifyFailure = Exclude<(typeof OWNER_VERIFY_STATES)[number], "ok">;
5
+ export type OwnerTurnVerdict = {
6
+ readonly kind: "unchecked";
7
+ } | {
8
+ readonly kind: "verified";
9
+ readonly replay: {
10
+ readonly channelId: string;
11
+ readonly groupId: string;
12
+ readonly epoch: number;
13
+ };
14
+ } | {
15
+ readonly kind: "constants_only";
16
+ readonly replay: {
17
+ readonly channelId: string;
18
+ readonly groupId: string;
19
+ readonly epoch: number;
20
+ };
21
+ } | {
22
+ readonly kind: "failed";
23
+ readonly reason: OwnerVerifyFailure;
24
+ };
25
+ export type OwnerSignatureResult = {
26
+ readonly state: "ok";
27
+ readonly envelope: AgentVaultEnvelope;
28
+ } | {
29
+ readonly state: "no_signature" | "bad_signature" | "malformed" | "unknown_algo";
30
+ };
31
+ export declare function verifyOwnerSignature(entry: {
32
+ readonly payloadHex?: string;
33
+ readonly identitySignature?: string;
34
+ }, pin: OwnerGrantPin): OwnerSignatureResult;
35
+ export type OwnerDmLeafClass = "ok" | "unsigned_relay" | "foreign";
36
+ /**
37
+ * §7.2 check 2, decision D-4. Any number of owner leaves passes (multi-device:
38
+ * leaf credentials encode identity, not device). Fails closed on an empty tree,
39
+ * a non-basic leaf, or no owner leaf. A signed relay is admitted by
40
+ * `relayIdentityId` alone; the roster flag only chooses between
41
+ * `unsigned_relay` (today's constants) and `foreign` (zero), so it can never
42
+ * widen past today's constants.
43
+ */
44
+ export declare function classifyOwnerDmLeaves(leaves: readonly {
45
+ basic: boolean;
46
+ identity: string | null;
47
+ }[], allowed: {
48
+ ownerIdentityId: string;
49
+ agentIdentityId: string;
50
+ relayIdentityId?: string;
51
+ isServerRelay: (identityId: string) => boolean;
52
+ }): OwnerDmLeafClass;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The bridge's persona renderer (owner-signed tool grants, spec §7.12; plan 4 Task 8).
3
+ *
4
+ * Under a manifest the bridge writes IDENTITY.md, SOUL.md and AGENTS.md from the
5
+ * raw persona whose hash the owner signed, never the server's text. It is a port
6
+ * of render_persona (backend/app/services/runtime_config_render.py) and is pinned
7
+ * to it byte for byte by persona-render.vectors.json, which the backend test
8
+ * test_persona_render_vectors.py re-renders with the Python function.
9
+ *
10
+ * Python str.strip() semantics, not JavaScript trim (trim also removes U+FEFF).
11
+ * `tone` is rendered in a fixed field order (TONE_FIELD_ORDER, the JSONB key
12
+ * order the backend reads), never the payload's key order (rulings item 8(a)).
13
+ */
14
+ export declare const PERSONA_MAX_BYTES = 8192;
15
+ export type PersonaRender = {
16
+ readonly ok: true;
17
+ readonly files: {
18
+ "IDENTITY.md": string;
19
+ "SOUL.md": string;
20
+ "AGENTS.md": string;
21
+ };
22
+ } | {
23
+ readonly ok: false;
24
+ readonly reason: "shape" | "over_cap";
25
+ };
26
+ /** Postgres JSONB returns object keys shorter-first, then bytewise; render_persona iterates that order. */
27
+ export declare const TONE_FIELD_ORDER: readonly ["humor", "length", "warmth", "formality"];
28
+ export declare function pyStrip(s: string): string;
29
+ /** A port of render_persona(persona, tool_names). Never throws. */
30
+ export declare function renderPersonaFiles(persona: unknown): PersonaRender;
@@ -0,0 +1,31 @@
1
+ export type PinCommandStep = "pin_tmp_written" | "cred_tmp_written" | "pin_renamed" | "cred_renamed";
2
+ export type PinCommandResult = {
3
+ ok: true;
4
+ changedKey: boolean;
5
+ } | {
6
+ ok: false;
7
+ reason: "pin_invalid" | "credential_invalid" | "symlink_refused" | "agent_mismatch" | "directory_mismatch" | "io_error";
8
+ };
9
+ export declare const PIN_COPY: {
10
+ readonly saved: string;
11
+ readonly pinInvalid: string;
12
+ readonly credentialInvalid: string;
13
+ readonly symlink: string;
14
+ readonly ioError: string;
15
+ readonly agentMismatch: string;
16
+ readonly directoryMismatch: string;
17
+ readonly announce: (agentIdentityId: string) => string;
18
+ readonly usage: string;
19
+ };
20
+ export declare function runPinCommand(opts: {
21
+ credentialPath: string;
22
+ pinSourcePath: string;
23
+ agentIdentityId: string;
24
+ /** Required. Called exactly once, after every check passes and before the first write, with the credential's identity_id. Never called on a refusal. */
25
+ announce: (agentIdentityId: string) => void;
26
+ afterStep?: (s: PinCommandStep) => void;
27
+ }): PinCommandResult;
28
+ export declare function runPinCli(argv: readonly string[], io: {
29
+ out: (line: string) => void;
30
+ err: (line: string) => void;
31
+ }): 0 | 1 | 2;