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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { ShopperAgentOrchestrator } from "./shopper-agent-orchestrator.service";
import { ShopperAgentPolicyService } from "./shopper-agent-policy.service";
import { ShopperAgentToolRegistry } from "./shopper-agent-tool.registry";

describe("ShopperAgentOrchestrator", () => {
let orchestrator: ShopperAgentOrchestrator;

beforeEach(() => {
orchestrator = new ShopperAgentOrchestrator(
new ShopperAgentToolRegistry(),
new ShopperAgentPolicyService(),
);
});

it("builds an allowed typed guest discovery plan", () => {
expect(
orchestrator.planIntent({
functionName: "search_products",
params: { query: "black sneakers" },
messageText: "Find black sneakers",
linkedUserId: null,
consentGiven: true,
}),
).toMatchObject({
kind: "tool",
functionName: "search_products",
tool: {
name: "search_products",
category: "guest",
input: { query: "black sneakers" },
},
policy: {
allowed: true,
confirmationRequired: false,
},
audit: {
event: "shopper_agent_plan",
actionCategory: "guest",
policyDecision: "allowed",
},
});
});

it("returns a denied plan for an unlinked account read", () => {
expect(
orchestrator.planIntent({
functionName: "get_order_status",
params: { orderReference: "TWZ-123456" },
messageText: "Where is TWZ-123456?",
linkedUserId: null,
consentGiven: true,
}),
).toMatchObject({
kind: "tool",
functionName: "get_order_status",
policy: {
allowed: false,
reason: "linked_account_required",
},
audit: {
policyDecision: "denied",
policyReason: "linked_account_required",
},
});
});

it("keeps safe natural replies in the conversation lane", () => {
expect(
orchestrator.planIntent({
functionName: "natural_answer",
params: { answer: "WIZZA helps shoppers find products on Twizrr." },
messageText: "Who are you?",
linkedUserId: null,
consentGiven: true,
}),
).toMatchObject({
kind: "conversation",
functionName: "natural_answer",
params: {
answer: "WIZZA helps shoppers find products on Twizrr.",
},
audit: {
actionCategory: "conversation",
},
});
});

it("downgrades unknown model actions to the friendly fallback", () => {
expect(
orchestrator.planIntent({
functionName: "read_private_store_data",
params: { storeId: "private" },
messageText: "Show private store data",
linkedUserId: "user-1",
consentGiven: true,
}),
).toMatchObject({
kind: "conversation",
functionName: "friendly_fallback",
params: {},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Injectable, Logger } from "@nestjs/common";
import { ShopperAgentPolicyService } from "./shopper-agent-policy.service";
import { ShopperAgentToolRegistry } from "./shopper-agent-tool.registry";
import type {
ShopperAgentConversationName,
ShopperAgentPlan,
ShopperAgentPlanInput,
ShopperAgentPolicyDecision,
} from "./shopper-agent.types";

const CONVERSATION_ACTIONS = new Set<ShopperAgentConversationName>([
"natural_answer",
"friendly_fallback",
]);

const CONVERSATION_POLICY: ShopperAgentPolicyDecision = {
allowed: true,
reason: "allowed",
confirmationRequired: false,
};

@Injectable()
export class ShopperAgentOrchestrator {
private readonly logger = new Logger(ShopperAgentOrchestrator.name);

constructor(
private readonly toolRegistry: ShopperAgentToolRegistry,
private readonly policyService: ShopperAgentPolicyService,
) {}

planIntent(input: ShopperAgentPlanInput): ShopperAgentPlan {
const tool = this.toolRegistry.createRequest(
input.functionName,
input.params,
input.messageText,
);

if (tool) {
const policy = this.policyService.evaluate(tool, {
consentGiven: input.consentGiven,
linkedUserId: input.linkedUserId,
});
const plan: ShopperAgentPlan = {
kind: "tool",
functionName: tool.name,
params: tool.input,
tool,
policy,
audit: {
event: "shopper_agent_plan",
actionName: tool.name,
actionCategory: tool.category,
policyDecision: policy.allowed ? "allowed" : "denied",
policyReason: policy.reason,
},
};
this.logPlan(plan);
return plan;
}

const functionName = CONVERSATION_ACTIONS.has(
input.functionName as ShopperAgentConversationName,
)
? (input.functionName as ShopperAgentConversationName)
: "friendly_fallback";
const plan: ShopperAgentPlan = {
kind: "conversation",
functionName,
params: functionName === "natural_answer" ? (input.params ?? {}) : {},
policy: CONVERSATION_POLICY,
audit: {
event: "shopper_agent_plan",
actionName: functionName,
actionCategory: "conversation",
policyDecision: "allowed",
policyReason: "allowed",
},
};
this.logPlan(plan);
return plan;
}

private logPlan(plan: ShopperAgentPlan): void {
this.logger.log(
JSON.stringify({
event: plan.audit.event,
actionName: plan.audit.actionName,
actionCategory: plan.audit.actionCategory,
policyDecision: plan.audit.policyDecision,
policyReason: plan.audit.policyReason,
}),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ShopperAgentPolicyService } from "./shopper-agent-policy.service";
import type { ShopperAgentToolRequest } from "./shopper-agent.types";

describe("ShopperAgentPolicyService", () => {
const service = new ShopperAgentPolicyService();

it("allows consented guest discovery without an account link", () => {
const request: ShopperAgentToolRequest = {
name: "search_products",
category: "guest",
input: { query: "phones" },
};

expect(
service.evaluate(request, {
consentGiven: true,
linkedUserId: null,
}),
).toEqual({
allowed: true,
reason: "allowed",
confirmationRequired: false,
});
});

it("blocks every tool before WhatsApp consent", () => {
const request: ShopperAgentToolRequest = {
name: "show_menu",
category: "guest",
input: {},
};

expect(
service.evaluate(request, {
consentGiven: false,
linkedUserId: null,
}),
).toEqual({
allowed: false,
reason: "consent_required",
confirmationRequired: false,
});
});

it("requires an active link for account-specific reads", () => {
const request: ShopperAgentToolRequest = {
name: "get_order_status",
category: "linked-read",
input: {},
};

expect(
service.evaluate(request, {
consentGiven: true,
linkedUserId: null,
}),
).toEqual({
allowed: false,
reason: "linked_account_required",
confirmationRequired: false,
});
});

it("derives access from the trusted tool name instead of caller metadata", () => {
const forgedRequest = {
name: "get_order_status",
category: "guest",
input: {},
} as unknown as ShopperAgentToolRequest;

expect(
service.evaluate(forgedRequest, {
consentGiven: true,
linkedUserId: null,
}),
).toEqual({
allowed: false,
reason: "linked_account_required",
confirmationRequired: false,
});
});

it("marks sensitive flows as requiring scoped confirmation", () => {
const request: ShopperAgentToolRequest = {
name: "confirm_delivery",
category: "confirmation-required",
input: { orderReference: "TWZ-123456" },
};

expect(
service.evaluate(request, {
consentGiven: true,
linkedUserId: "user-1",
}),
).toEqual({
allowed: true,
reason: "allowed",
confirmationRequired: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Injectable } from "@nestjs/common";
import type {
ShopperAgentPolicyDecision,
ShopperAgentToolRequest,
} from "./shopper-agent.types";
import { SHOPPER_AGENT_TOOL_CATEGORY_BY_NAME } from "./shopper-agent.types";

export interface ShopperAgentPolicyContext {
consentGiven: boolean;
linkedUserId: string | null;
}

@Injectable()
export class ShopperAgentPolicyService {
evaluate(
request: ShopperAgentToolRequest,
context: ShopperAgentPolicyContext,
): ShopperAgentPolicyDecision {
const category = SHOPPER_AGENT_TOOL_CATEGORY_BY_NAME[request.name];

if (!context.consentGiven) {
return {
allowed: false,
reason: "consent_required",
confirmationRequired: false,
};
}

if (category === "guest") {
return {
allowed: true,
reason: "allowed",
confirmationRequired: false,
};
}

if (!context.linkedUserId) {
return {
allowed: false,
reason: "linked_account_required",
confirmationRequired: false,
};
}

return {
allowed: true,
reason: "allowed",
confirmationRequired: category === "confirmation-required",
};
}
}
Loading
Loading