Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/core/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export const MCP_TOOL_PARAMS = z.object({
/** The redeemed code's own signature field (gateway_redeem_connection_code), or a PGP fingerprint the caller already independently trusts, used either to pin a supplied publicKey or to fetch one from a keyserver when publicKey is omitted. */
signature: z.string().optional(),
fingerprint: z.string().optional(),
/** For gateway_trust/gateway_untrust: when true, `device` names a user principal (agent-comms#187) rather than a bare remote device-id (agent-comms#193). */
principal: z.boolean().optional(),
});

export type ToolParams = z.infer<typeof MCP_TOOL_PARAMS>;
Expand Down Expand Up @@ -400,11 +402,19 @@ export function buildAction(params: Record<string, unknown>): CommsAction {
case "gateway_trust":
if (p.device === undefined)
throw new BuildActionError("gateway_trust", "device");
return { action: "gateway_trust", device: p.device };
return {
action: "gateway_trust",
device: p.device,
...(p.principal !== undefined && { principal: p.principal }),
};
case "gateway_untrust":
if (p.device === undefined)
throw new BuildActionError("gateway_untrust", "device");
return { action: "gateway_untrust", device: p.device };
return {
action: "gateway_untrust",
device: p.device,
...(p.principal !== undefined && { principal: p.principal }),
};
case "gateway_list_trusted":
return { action: "gateway_list_trusted" };
case "gateway_generate_connection_code":
Expand Down
96 changes: 96 additions & 0 deletions src/core/gateway-trust-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* CommsTool's gateway-trust action handlers (agent-comms#156, extended by agent-comms#187's principal-keyed allowlist and agent-comms#193's tool-level `principal` flag) -- split out of tool.ts purely to keep that file under the repo's max-lines cap, the same reason agent-registry.ts, delivery-engine.ts, and their siblings were split from mesh-store.ts. Free functions over MeshOnlyFeatures's own gateway-trust methods rather than class methods, since CommsTool owns no state of its own for this concern -- every one of these is a pure translation from a CommsAction to a CommsResult against whatever store methods are present.
*/
import type { CommsAction } from "./types.js";
import type { CommsResult, MeshOnlyFeatures } from "./tool.js";

/** The slice of MeshOnlyFeatures the three functions below actually need, named so this file doesn't repeat the same six-method Pick inline at every signature. */
export type GatewayTrustStore = Pick<
MeshOnlyFeatures,
| "addTrustedGateway"
| "removeTrustedGateway"
| "listTrustedGateways"
| "addTrustedGatewayPrincipal"
| "removeTrustedGatewayPrincipal"
| "listTrustedGatewayPrincipals"
>;

/** Uniform "gateway trust isn't available on this store" result, mirroring tool.ts's own notMeshBacked helper for the other MeshOnlyFeatures methods. */
function gatewayTrustUnavailable(): CommsResult {
return {
content: "Gateway trust is not available on this store.",
isError: true,
};
}

/** Trusts action.device: as a user principal (GatewayTrust.addPrincipal, agent-comms#187) when action.principal is true, or as a bare remote device-id (the original agent-comms#156 behaviour) otherwise. */
export function gatewayTrust(
store: Readonly<GatewayTrustStore>,
action: CommsAction & { action: "gateway_trust" },
): CommsResult {
if (action.principal === true) {
if (!store.addTrustedGatewayPrincipal) return gatewayTrustUnavailable();
store.addTrustedGatewayPrincipal(action.device);
return {
content: `Trusted remote gateway principal ${action.device}.`,
isError: false,
};
}
if (!store.addTrustedGateway) return gatewayTrustUnavailable();
store.addTrustedGateway(action.device);
return {
content: `Trusted remote gateway device ${action.device}.`,
isError: false,
};
}

/** Withdraws trust from action.device: from the principal allowlist (GatewayTrust.removePrincipal, agent-comms#187) when action.principal is true, or from the bare-device allowlist (the original agent-comms#156 behaviour) otherwise. */
export function gatewayUntrust(
store: Readonly<GatewayTrustStore>,
action: CommsAction & { action: "gateway_untrust" },
): CommsResult {
if (action.principal === true) {
if (!store.removeTrustedGatewayPrincipal) return gatewayTrustUnavailable();
store.removeTrustedGatewayPrincipal(action.device);
return {
content: `Untrusted remote gateway principal ${action.device}.`,
isError: false,
};
}
if (!store.removeTrustedGateway) return gatewayTrustUnavailable();
store.removeTrustedGateway(action.device);
return {
content: `Untrusted remote gateway device ${action.device}.`,
isError: false,
};
}

/** Reports every currently trusted remote device-id and every currently trusted principal device-id (agent-comms#187) together -- listing is never mutually exclusive between the two sets, so, unlike gatewayTrust/gatewayUntrust above, this needs no `principal` flag of its own. */
export function gatewayListTrusted(
store: Readonly<GatewayTrustStore>,
): CommsResult {
if (!store.listTrustedGateways) return gatewayTrustUnavailable();
const devices = store.listTrustedGateways();
const principals = store.listTrustedGatewayPrincipals?.() ?? [];
if (devices.length === 0 && principals.length === 0) {
return {
content: "No remote gateway devices or principals are trusted.",
isError: false,
};
}
const sections: string[] = [];
if (devices.length > 0) {
sections.push(
`Trusted remote gateway devices:\n${devices.map((device) => ` ${device}`).join("\n")}`,
);
}
if (principals.length > 0) {
sections.push(
`Trusted remote gateway principals:\n${principals.map((principal) => ` ${principal}`).join("\n")}`,
);
}
return {
content: sections.join("\n"),
isError: false,
};
}
15 changes: 15 additions & 0 deletions src/core/mesh-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,21 @@ export class MeshStore implements CommsStore {
return this.gatewayTrust.list();
}

/** Trusts a remote user-principal device-id (hex, agent-comms#187): a peer presenting a token whose delegation chain roots at this principal is trusted via GatewayTrust.isTrustedFor, without its own bare device-id ever needing individual trust. Entirely independent of the bare-device allowlist addTrustedGateway manages. */
addTrustedGatewayPrincipal(deviceHex: string): void {
this.gatewayTrust.addPrincipal(deviceHex);
}

/** Withdraws trust from a remote user-principal device-id (hex). A no-op if it was never trusted. */
removeTrustedGatewayPrincipal(deviceHex: string): void {
this.gatewayTrust.removePrincipal(deviceHex);
}

/** Every currently trusted remote user-principal device-id (hex). */
listTrustedGatewayPrincipals(): string[] {
return this.gatewayTrust.listPrincipals();
}

// -----------------------------------------------------------------------
// Connection codes (agent-comms#188) -- bootstrapping gateway trust with no existing mesh connection between the two devices
// -----------------------------------------------------------------------
Expand Down
66 changes: 11 additions & 55 deletions src/core/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ import {
handleGatewayGenerateConnectionCode,
handleGatewayRedeemConnectionCode,
} from "./connection-code-tool.js";
import {
gatewayTrust,
gatewayUntrust,
gatewayListTrusted,
} from "./gateway-trust-actions.js";

/** Table column widths for the plain-text listing helpers below, chosen to line up with the existing aligned output. */
const ROOM_TYPE_COLUMN_WIDTH = 7;
Expand Down Expand Up @@ -124,6 +129,9 @@ export interface MeshOnlyFeatures {
candidate: Readonly<ConnectionCode>,
options: Readonly<RedeemConnectionCodeOptions>,
) => Promise<RedeemConnectionCodeResult>;
addTrustedGatewayPrincipal?: (deviceHex: string) => void;
removeTrustedGatewayPrincipal?: (deviceHex: string) => void;
listTrustedGatewayPrincipals?: () => string[];
}

/** Uniform "this bridge isn't backed by a mesh transport" result for a MeshOnlyFeatures method that isn't present on the current store. */
Expand Down Expand Up @@ -255,11 +263,11 @@ export class CommsTool {
case "mesh_get_visibility":
return this.meshGetVisibility(action);
case "gateway_trust":
return this.gatewayTrust(action);
return gatewayTrust(this.store, action);
case "gateway_untrust":
return this.gatewayUntrust(action);
return gatewayUntrust(this.store, action);
case "gateway_list_trusted":
return this.gatewayListTrusted();
return gatewayListTrusted(this.store);
case "gateway_generate_connection_code":
return await handleGatewayGenerateConnectionCode(this.store, action);
case "gateway_redeem_connection_code":
Expand Down Expand Up @@ -678,58 +686,6 @@ export class CommsTool {
};
}

private gatewayTrust(
action: CommsAction & { action: "gateway_trust" },
): CommsResult {
if (!this.store.addTrustedGateway) {
return {
content: "Gateway trust is not available on this store.",
isError: true,
};
}
this.store.addTrustedGateway(action.device);
return {
content: `Trusted remote gateway device ${action.device}.`,
isError: false,
};
}

private gatewayUntrust(
action: CommsAction & { action: "gateway_untrust" },
): CommsResult {
if (!this.store.removeTrustedGateway) {
return {
content: "Gateway trust is not available on this store.",
isError: true,
};
}
this.store.removeTrustedGateway(action.device);
return {
content: `Untrusted remote gateway device ${action.device}.`,
isError: false,
};
}

private gatewayListTrusted(): CommsResult {
if (!this.store.listTrustedGateways) {
return {
content: "Gateway trust is not available on this store.",
isError: true,
};
}
const trusted = this.store.listTrustedGateways();
if (trusted.length === 0) {
return {
content: "No remote gateway devices are trusted.",
isError: false,
};
}
return {
content: `Trusted remote gateway devices:\n${trusted.map((device) => ` ${device}`).join("\n")}`,
isError: false,
};
}

private async meshConnect(
_ctx: Readonly<CommsContext>,
action: CommsAction & { action: "mesh_connect" },
Expand Down
4 changes: 4 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,14 @@ export const CommsActionSchema = defineSchema(
z.object({
action: z.literal("gateway_trust"),
device: z.string(),
/** When true, `device` is trusted as a user principal (GatewayTrust.addPrincipal, agent-comms#187) rather than a bare remote device-id (agent-comms#193). Omitted or false keeps the original bare-device behaviour. */
principal: z.boolean().optional(),
}),
z.object({
action: z.literal("gateway_untrust"),
device: z.string(),
/** When true, `device` is withdrawn from the principal allowlist (GatewayTrust.removePrincipal, agent-comms#187) rather than the bare-device one (agent-comms#193). Omitted or false keeps the original bare-device behaviour. */
principal: z.boolean().optional(),
}),
z.object({ action: z.literal("gateway_list_trusted") }),
z.object({
Expand Down
Loading
Loading