@securecoms/agent-bridge 0.34.0 → 0.36.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * MCP client Phase 1, Task 14 (spec §12.2, §13a; M-D ruling and its remainder at 3c203f8bb): the signed `mcp` section.
3
+ *
4
+ * "mcp": { "servers": { "<server_id>": {
5
+ * "vendor": "<catalog key>", "url": "<canonical https URL>",
6
+ * "auth": { "mode": "oauth", "issuer": "<https URL>", "client": "securecoms_cimd" }
7
+ * | { "mode": "oauth", "issuer": "<https URL>", "client": "customer", "client_id": "<string>" }
8
+ * | { "mode": "key" },
9
+ * "tools": { "<tool>": { "sha256": "<hex64>", "class": "read" } } } },
10
+ * "removed"?: [ "<server_id>", ... ] | "*" }
11
+ *
12
+ * `removed` (C-NEW-1 architect ruling, 2026-10-30): the owner's explicit, per-save removal. Absence of a server from
13
+ * `servers` NEVER deletes its credential; only a server named here (or `"*"`, remove-all) does. Optional; absent is `[]`.
14
+ * An array holds 1 to 8 distinct valid, non-reserved server ids, none of them also a key of `servers`; `"*"` is valid
15
+ * only with `servers` empty. Anything else makes the section malformed (non-deleting).
16
+ *
17
+ * EXACT keys everywhere and NO PATH anywhere (M-D): the host derives every credential path from the server id, so a
18
+ * `token_file`, `key_file` or `client_secret_file` key (or any other key) makes the section malformed. The client-kind
19
+ * key is `client`, with the #1315 §3.1 names; `securecoms_installed` (a Google desktop client) is not an MCP client and
20
+ * is refused. A bad section returns null; the CALLER keeps the rest of the manifest (T-15), which departs on purpose from
21
+ * the all-or-nothing rule of the other sections.
22
+ *
23
+ * Pure. Comparison with the bridge's catalog copy (vendor present, url equals `readOnlyUrl`, issuer equals the row) is a
24
+ * per-server verdict (`mcpServerAcceptable`), never a section parse failure: spec §6.5 lists those as server `refused`.
25
+ */
26
+ import type { McpCatalog, McpCatalogRow } from "./mcp-catalog.js";
27
+ export declare const MCP_MAX_SERVERS = 8;
28
+ export declare const MCP_MAX_TOOLS_PER_SERVER = 64;
29
+ export declare const MCP_MAX_SIGNED_TOOLS = 160;
30
+ /** C-NEW-1: at most this many server ids in one signed `removed` list. */
31
+ export declare const MCP_MAX_REMOVED = 8;
32
+ /** OQ-5 ruling: the SecureComs Client ID Metadata Document. A `securecoms_cimd` client's client_id is this URL, derived on the host. */
33
+ export declare const SECURECOMS_CIMD_CLIENT_ID = "https://app.securecoms.ai/oauth/securecoms-agent-client.json";
34
+ export declare const MCP_SERVER_ID: RegExp;
35
+ export declare const MCP_TOOL_NAME: RegExp;
36
+ /**
37
+ * PR-B1 audit M-1: names a JS object treats specially. A signed `__proto__` would set the map's prototype instead of a
38
+ * key; `constructor` and `prototype` would collide with inherited members. Refused as tool names and server ids (the
39
+ * section is malformed), and every map is built with no prototype as well.
40
+ */
41
+ export declare const MCP_RESERVED_NAMES: ReadonlySet<string>;
42
+ export type McpClientKind = "securecoms_cimd" | "customer";
43
+ export type SignedMcpAuth = {
44
+ readonly mode: "oauth";
45
+ readonly issuer: string;
46
+ readonly client: "securecoms_cimd";
47
+ } | {
48
+ readonly mode: "oauth";
49
+ readonly issuer: string;
50
+ readonly client: "customer";
51
+ readonly client_id: string;
52
+ } | {
53
+ readonly mode: "key";
54
+ };
55
+ export interface SignedMcpTool {
56
+ readonly sha256: string;
57
+ readonly class: "read";
58
+ }
59
+ export interface SignedMcpServer {
60
+ readonly vendor: string;
61
+ readonly url: string;
62
+ readonly auth: SignedMcpAuth;
63
+ readonly tools: Readonly<Record<string, SignedMcpTool>>;
64
+ }
65
+ /** The signed removal: server ids the owner removed in this save, or `"*"` (remove-all). `[]` when the key is absent. */
66
+ export type SignedMcpRemoved = readonly string[] | "*";
67
+ export interface SignedMcp {
68
+ readonly servers: Readonly<Record<string, SignedMcpServer>>;
69
+ /** C-NEW-1: the ONLY source of a credential deletion on top `ok`. Never inferred from absence. */
70
+ readonly removed: SignedMcpRemoved;
71
+ }
72
+ /** The exact key sets per auth shape. Anything else, path keys included, is refused (M-D). */
73
+ export declare const MCP_AUTH_KEYS: Readonly<{
74
+ oauth_securecoms_cimd: readonly string[];
75
+ oauth_customer: readonly string[];
76
+ key: readonly string[];
77
+ }>;
78
+ /** The signed `mcp` value, or null when it is not exactly the §13a shape (the section is then `malformed`, non-deleting). */
79
+ export declare function parseMcp(v: unknown): SignedMcp | null;
80
+ /**
81
+ * Per-server catalog verdict: the vendor is in the bridge's catalog, the signed url is the row's read-only URL exactly,
82
+ * and (oauth) the signed issuer is the row's issuer (audit r1 C-1). The row on success, else null (server `refused`).
83
+ */
84
+ export declare function mcpServerCatalogRow(s: SignedMcpServer, catalog: McpCatalog): McpCatalogRow | null;
85
+ /** `mcp:<server_id>/<tool>` split, or null for any other shape. */
86
+ export declare function parseMcpGrant(name: string): {
87
+ serverId: string;
88
+ tool: string;
89
+ } | null;
90
+ /**
91
+ * Spec §12.2 code ceiling for one owner_dm grant string: the server and tool are signed in the `mcp` section, the
92
+ * server passes the catalog verdict, and the tool is on the catalog row's read list. team_room accepts none (caller).
93
+ */
94
+ export declare function mcpGrantWithinCeiling(name: string, mcp: SignedMcp | null, catalog: McpCatalog): boolean;
@@ -2,6 +2,8 @@ import type { AgentVaultEnvelope } from "@agentvault/connect-core";
2
2
  import { type OwnerGrantPin } from "./pin.js";
3
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
4
  export type OwnerVerifyFailure = Exclude<(typeof OWNER_VERIFY_STATES)[number], "ok">;
5
+ /** Phase 0 Task 5: what the is_dm === true branch read from the DM's leaf set. */
6
+ export type OwnerDmRelay = "none" | "signed" | "unsigned";
5
7
  export type OwnerTurnVerdict = {
6
8
  readonly kind: "unchecked";
7
9
  } | {
@@ -11,6 +13,8 @@ export type OwnerTurnVerdict = {
11
13
  readonly groupId: string;
12
14
  readonly epoch: number;
13
15
  };
16
+ readonly relay?: OwnerDmRelay;
17
+ readonly relayLeaf: boolean;
14
18
  } | {
15
19
  readonly kind: "constants_only";
16
20
  readonly replay: {
@@ -18,6 +22,7 @@ export type OwnerTurnVerdict = {
18
22
  readonly groupId: string;
19
23
  readonly epoch: number;
20
24
  };
25
+ readonly relay?: OwnerDmRelay;
21
26
  } | {
22
27
  readonly kind: "failed";
23
28
  readonly reason: OwnerVerifyFailure;
@@ -50,3 +55,38 @@ export declare function classifyOwnerDmLeaves(leaves: readonly {
50
55
  relayIdentityId?: string;
51
56
  isServerRelay: (identityId: string) => boolean;
52
57
  }): OwnerDmLeafClass;
58
+ /**
59
+ * MCP Phase 1 Task 15 (H-3, OQ-2 ruling / ADR D): relay-free means the owner DM's local leaf set holds ONLY the pinned
60
+ * owner (any number of devices: leaf credentials encode identity, not device) and the agent, and both are present.
61
+ * A signed relay leaf, which classifyOwnerDmLeaves admits as "ok", is NOT relay-free. Fails closed on an empty tree,
62
+ * a non-basic leaf, or a leaf with no identity.
63
+ */
64
+ export declare function ownerDmLeavesRelayFree(leaves: readonly {
65
+ basic: boolean;
66
+ identity: string | null;
67
+ }[], ids: {
68
+ ownerIdentityId: string;
69
+ agentIdentityId: string;
70
+ }): boolean;
71
+ /**
72
+ * Phase 0 spec round 2 H-4: relayLeaf is derived from the verdict's Phase 0 `relay` field, at the one place
73
+ * verifyOwnerTurn builds a verified verdict. Only exactly "none" is relay-free; "signed", "unsigned", any unknown value
74
+ * and an absent value (classification did not run) all count as a relay (fail closed).
75
+ */
76
+ export declare function relayLeafFromPhase0Relay(relay: unknown): boolean;
77
+ /**
78
+ * Phase 0 Task 5 (spec §3.4): the verdict's `relay`, from the SAME leaf set and leaf class the is_dm === true branch
79
+ * just computed. Called only after the `foreign` return. "none" exactly when the set is relay-free (only the pinned
80
+ * owner, any number of devices, and the agent, both present: ownerDmLeavesRelayFree), so relayLeaf is unchanged by
81
+ * construction. "signed" only when a leaf really carries the manifest's signed relay identity (never from the
82
+ * manifest alone). Anything else (an ok class that is neither, e.g. no agent leaf) is undefined: not classified, which
83
+ * every reader treats as relay present.
84
+ */
85
+ export declare function ownerDmRelayOf(leafClass: Exclude<OwnerDmLeafClass, "foreign">, leaves: readonly {
86
+ basic: boolean;
87
+ identity: string | null;
88
+ }[], ids: {
89
+ ownerIdentityId: string;
90
+ agentIdentityId: string;
91
+ relayIdentityId?: string;
92
+ }): OwnerDmRelay | undefined;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Tool status, Phase 0 (spec §3.2, §3.3, §3.5; plan Task 7): the pure core of the owner-DM last-turn record.
3
+ *
4
+ * ADVISORY ONLY. Nothing reads the record to decide a grant: it is a content-free summary of a decision
5
+ * authorizationFor already made, posted for the owner's agent page. Enums, booleans, one 15-minute bucket `at`,
6
+ * one counter, and built-in tool names from the fixed allowlist below. Never a channel id, sender id, MCP tool name,
7
+ * argument or message data.
8
+ */
9
+ import type { GrantSource, WithheldWhy } from "./effective.js";
10
+ import type { OwnerTurnVerdict } from "./owner-turn.js";
11
+ /** Spec §3.2: current_time plus the seven MANIFEST_KNOWN_TOOLS.owner_dm names. Never an `mcp:` name. */
12
+ export declare const TOOL_STATUS_ALLOWLIST: readonly ["current_time", "web_search", "web_extract", "fetch_url", "http_request", "sql_query", "weather_forecast", "currency_convert"];
13
+ export declare const CAUSES: readonly ["owner_dm_unavailable", "manifest_switch_off", "unexpected_member_in_dm", "owner_turn_failed", "agent_state_error", "dm_channel_mismatch", "dm_not_linked", "unsigned_relay_in_dm", "manifest_not_bound", "no_runtime_consumer"];
14
+ export type Cause = (typeof CAUSES)[number];
15
+ export declare const SUBS: readonly ["kill_switch", "pin", "sender_not_owner", "stale"];
16
+ export type Sub = (typeof SUBS)[number];
17
+ export type RecordRelay = "none" | "signed" | "unsigned" | "unknown";
18
+ /**
19
+ * Architect ruling M-2 (A1 §8): the operator sees owner-DM activity at most as a 15-minute UTC bucket. `at` is the
20
+ * bucket start, and the sender posts only just after a bucket boundary (tool-status-sender.ts).
21
+ */
22
+ export declare const TOOL_STATUS_BUCKET_MS = 900000;
23
+ /** The start of the 15-minute UTC bucket holding `ms`, as `YYYY-MM-DDTHH:MMZ`. */
24
+ export declare function bucketStartAt(ms: number): string;
25
+ /** The instant the bucket that starts at `at` closes. */
26
+ export declare function bucketEndMs(at: string): number;
27
+ export interface OwnerDmTurnRecord {
28
+ /**
29
+ * Architect ruling M-2: the start of the 15-minute UTC bucket (bridge clock) in which the tuple last changed,
30
+ * `YYYY-MM-DDTHH:MMZ` with a minute of 00, 15, 30 or 45 (bucketStartAt). Not in the compared tuple.
31
+ */
32
+ readonly at: string;
33
+ /** toolGrantCounter: the manifest counter the turn ran under, null before any manifest. */
34
+ readonly counter: number | null;
35
+ readonly source: GrantSource;
36
+ readonly cause?: Cause;
37
+ readonly sub?: Sub;
38
+ readonly relay: RecordRelay;
39
+ readonly issued: readonly string[];
40
+ readonly withheld: readonly {
41
+ readonly tool: string;
42
+ readonly why: WithheldWhy;
43
+ }[];
44
+ readonly mcp_issued: number;
45
+ }
46
+ export interface CauseInput {
47
+ readonly killSwitchOff: boolean;
48
+ readonly manifestSwitchOff: boolean;
49
+ readonly pinMissingOrAnomalous: boolean;
50
+ /** Computed only from a PRESENT roster entry (OQ-5): a TTL-expired roster is `stale`, never this. */
51
+ readonly senderNotOwner: boolean;
52
+ /** `!ownerDmFresh || freshRosterEntry(channelId) === undefined` (OQ-5). */
53
+ readonly stale: boolean;
54
+ /** Never `unchecked` (correction 9: the recorded set is the grant-pin owner, so the verdict was checked). */
55
+ readonly verdict: Exclude<OwnerTurnVerdict, {
56
+ kind: "unchecked";
57
+ }>;
58
+ readonly manifestPresent: boolean;
59
+ readonly signedDmChannel: string | undefined;
60
+ readonly channelId: string;
61
+ readonly boundNow: boolean;
62
+ readonly consumerAttached: boolean;
63
+ }
64
+ export interface CauseResult {
65
+ readonly source: GrantSource;
66
+ readonly cause?: Cause;
67
+ readonly sub?: Sub;
68
+ }
69
+ /**
70
+ * Spec §3.3, first match wins, in the order 1, 2, 3, 3a, 4, 5, 6, 6a, 7, 8, 9, 10, 11. `sub` only for
71
+ * owner_dm_unavailable. `cause` omitted exactly when `source === "manifest"`. Source mirrors authorizationFor.
72
+ * Returns null for a failed `replay` verdict (not recorded) and never throws (round 1 M-3: it runs inside the
73
+ * authorization path); any unexpected input is null too.
74
+ */
75
+ export declare function ownerDmCause(i: CauseInput): CauseResult | null;
76
+ /** Spec §3.5 change tuple {source, cause, sub, relay, issued, withheld, mcp_issued, counter}; `at` excluded (round 2 M-8), `sub` included (OQ-4). */
77
+ export declare function sameTuple(a: OwnerDmTurnRecord | null | undefined, b: OwnerDmTurnRecord | null | undefined): boolean;
78
+ /** Sorted, unique, allowlist only. Drops every `mcp:` name and any tool outside the fixed allowlist. */
79
+ export declare function sanitizeIssued(names: readonly string[]): string[];
@@ -28,6 +28,7 @@ export declare function deliverSseBatch(res: Parameters<typeof writeSse>[0], bat
28
28
  requeueFront(msgs: BridgeMessage[]): void;
29
29
  harnessGrantsWithheld(): boolean;
30
30
  signedSearchProvider(): SearchProvider | undefined;
31
+ r3Hold(): boolean;
31
32
  }, onWriteError?: (error: unknown) => void): void;
32
33
  /**
33
34
  * Once-per-connection write-failure reporter (#335). A throwing `res.write`