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
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ If private vulnerability reporting is not available, open a minimal issue at <ht
- Customer approval is server state, never a model assertion, and is bound to an immutable fingerprint of the exact estimate contents.
- Money, compatibility, inventory, and schedule checks use deterministic code.
- Streamable HTTP deployments validate `Host` and `Origin`, authenticate remote callers, rate-limit requests, and avoid customer PII in logs.
- MCP HTTP input is capped at 64 KiB before buffering or JSON parsing, the SDK limits batches to 100 messages, and each tool call is capped at 1,000 combined array elements and object keys before schema validation.
- Secrets are supplied through environment variables or AWS Secrets Manager.
- The AWS narrator requires IAM/SigV4, not a public build marker. Its retained DynamoDB allowance reserves every model attempt atomically before execution, never refunds uncertain failures, and fails closed on missing/exhausted/unavailable state. The handler only conditionally decrements an existing allowance. The role's UpdateItem permission does not itself prevent initialization or refill by different or compromised code. This bounds model attempts, not all AWS charges.

Expand Down
2 changes: 1 addition & 1 deletion apps/alexa-simulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/credential-provider-node": "^3.972.82",
"@modelcontextprotocol/client": "^2.0.0",
"@modelcontextprotocol/client": "^2.3.1",
"@smithy/protocol-http": "^5.6.2",
"@smithy/signature-v4": "^5.7.3",
"express": "^5.2.1"
Expand Down
2 changes: 1 addition & 1 deletion infra/aws/bedrock-narrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The September 5 source revision addresses six baseline policy findings and recor
- **Allowance encryption:** the retained DynamoDB table explicitly uses a separate rotating customer-managed key. The narrator key policy grants only `kms:Decrypt`, constrained by the account, regional DynamoDB service and exact table encryption context. The deployment identity needs the appropriate key-management permissions; application runtime permissions are not deployment permissions.
- **Recovery:** DynamoDB point-in-time recovery uses a seven-day window. [The recovery runbook](recovery.md) prohibits reconnecting an old allowance snapshot without reconciliation; unknown consumption fails closed with zero remaining allowance. Backups are not authority to grant new model calls.
- **IAM governance:** the existing narrow Bedrock/DynamoDB statements move from inline policy into an attached customer-managed policy. The fixed DynamoDB partition-key condition also rejects a missing leading-key context. The existing AWS basic logging policy remains; this change does not claim that IAM makes the allowance immutable.
- **Scoped exceptions:** no dead-letter queue for this synchronous API workflow, and no customer VPC while there are no private-network dependencies. Re-review by October 5, 2026, or before either assumption changes. IAM ingress and TLS remain required. These are accepted findings, not claims that the raw Guard rules pass.
- **Scoped exceptions:** no dead-letter queue for this synchronous API workflow, and no customer VPC while there are no private-network dependencies. The owner re-reviewed and renewed these same two source exceptions on October 18, 2026. Re-review by November 18, 2026, or before either assumption changes. IAM ingress and TLS remain required. These are accepted findings, not claims that the raw Guard rules pass. This renewal records source approval only.

Quota history: the initial 102 request and subsequent owner-approved 1,001 request are recorded in [deployment preparation](../../../docs/verification/aws-deployment-preparation-2026-09-05.md). At the final review, request `9aee223c1a5b44e9a2d68fd7b3108dae0Lq1YWJJ` still reported **CASE_OPENED**, while the applied quota independently read **1,000**. This does not establish approval of 1,001. No quota request was changed during this deployment.

Expand Down
4 changes: 2 additions & 2 deletions infra/aws/bedrock-narrator/policy-exceptions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"approvedOn": "2026-09-05",
"reviewBy": "2026-10-05",
"approvedOn": "2026-10-18",
"reviewBy": "2026-11-18",
"resource": "NarratorFunction",
"exceptions": [
{
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@modelcontextprotocol/client": "^2.0.0",
"@modelcontextprotocol/client": "^2.3.1",
"@types/node": "^24.10.0",
"concurrently": "^9.2.1",
"concurrently": "^9.2.5",
"esbuild": "^0.28.2",
"eslint": "^9.39.1",
"tsx": "^4.23.13",
Expand Down
106 changes: 53 additions & 53 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions services/flo-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"@flo/agent": "workspace:*",
"@flo/domain": "workspace:*",
"@flo/shared-types": "workspace:*",
"@modelcontextprotocol/client": "^2.0.0",
"@modelcontextprotocol/node": "^2.0.0",
"@modelcontextprotocol/server": "^2.0.0",
"@modelcontextprotocol/client": "^2.3.1",
"@modelcontextprotocol/node": "^2.1.1",
"@modelcontextprotocol/server": "^2.3.1",
"zod": "^4.5.4"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion services/flo-mcp/src/customer-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/cli
import { CustomerAuthError, type CustomerWebsiteIdentity } from "@flo/agent";
import { z } from "zod";
import { createCustomerMcpServer, type CustomerRepairService } from "./customer-tools.js";
import { MCP_MAX_REQUEST_BYTES } from "./mcp-limits.js";

const sessionCookie = "__Host-flo-session";
const stateCookie = "__Host-flo-lwa-state";
Expand Down Expand Up @@ -70,7 +71,7 @@ export const createCustomerHttp = (options: CustomerHttpOptions) => {
const current = await auth.principal(bearer);
if (current.subject !== principal.subject || current.customerId !== principal.customerId) throw new CustomerAuthError(401, "SIGN_IN_REQUIRED");
};
const handler = createMcpHandler(() => createCustomerMcpServer(options.experience, principal, revalidate), { legacy: "stateless" });
const handler = createMcpHandler(() => createCustomerMcpServer(options.experience, principal, revalidate), { legacy: "stateless", maxRequestBodySize: MCP_MAX_REQUEST_BYTES });
try {
const response = await handler.fetch(request);
return new Response(await response.arrayBuffer(), { status: response.status, headers: response.headers });
Expand Down
2 changes: 2 additions & 0 deletions services/flo-mcp/src/customer-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import { customerRepairSchema, customerEstimateSchema, type CustomerExperience, type CustomerPrincipal } from "@flo/agent";
import { FloError } from "@flo/shared-types";
import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits.js";

export type CustomerRepairService = Pick<CustomerExperience, "listRepairs" | "getRepair" | "getEstimate">;
export const createCustomerMcpServer = (experience: CustomerRepairService, principal: CustomerPrincipal, revalidate?: () => Promise<void>): McpServer => {
const server = new McpServer({ name: "flo-customer", version: "0.2.0" }, {
maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
instructions: "Vehicle-owner repair status and estimate review. Read only. Never claim approval, payment, booking, or cancellation occurred. Never request contact details or a VIN by voice. Ask which repair when more than one matches."
});
const register = <I extends z.ZodRawShape, O extends z.ZodType>(name: string, description: string, inputSchema: z.ZodObject<I>, output: O, operation: (input: z.infer<z.ZodObject<I>>) => Promise<z.infer<O>>, summarize: (data: z.infer<O>) => string) => {
Expand Down
4 changes: 4 additions & 0 deletions services/flo-mcp/src/mcp-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Flo tool arguments are small identifiers and bounded operational text.
// Apply the same byte bound before Node buffering and SDK JSON parsing.
export const MCP_MAX_REQUEST_BYTES = 64 * 1024;
export const MCP_MAX_TOOL_INPUT_ELEMENTS = 1000;
9 changes: 5 additions & 4 deletions services/flo-mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Actor, Role } from "@flo/shared-types";
import { isDemoModeEnabled } from "./runtime-config.js";
import { createFloMcpServer } from "./tools.js";
import { createCustomerMcpServer } from "./customer-tools.js";
import { MCP_MAX_REQUEST_BYTES } from "./mcp-limits.js";

export { isDemoModeEnabled } from "./runtime-config.js";
export { createCustomerWebsite } from "./customer-website.js";
Expand Down Expand Up @@ -50,14 +51,14 @@ export const createFloHttpServer = (options: FloServerOptions = {}): { server: H
const adapters = options.adapters ?? defaultAdapters();
const orchestrator = new FloOrchestrator(adapters, new InMemoryJobMemoryStore(), options.clock);
const demoMode = options.demoMode ?? isDemoModeEnabled();
const handler = createMcpHandler(({ requestInfo }) => createFloMcpServer(orchestrator, actorFromRequest(requestInfo), { demoMode }), { legacy: "stateless" });
const nodeHandler = toNodeHandler(handler);
const handler = createMcpHandler(({ requestInfo }) => createFloMcpServer(orchestrator, actorFromRequest(requestInfo), { demoMode }), { legacy: "stateless", maxRequestBodySize: MCP_MAX_REQUEST_BYTES });
const nodeHandler = toNodeHandler(handler, { maxRequestBodySize: MCP_MAX_REQUEST_BYTES });
const customerExperience = new CustomerExperience(adapters.shop);
// Synthetic identity is fixed server-side. Production stays closed until verified
// OAuth account linking is implemented, including distinct service/user tokens.
const customerHandler = createMcpHandler(() => createCustomerMcpServer(customerExperience,
{ subject: "demo-owner", customerId: "customer-001" }), { legacy: "stateless" });
const customerNodeHandler = toNodeHandler(customerHandler);
{ subject: "demo-owner", customerId: "customer-001" }), { legacy: "stateless", maxRequestBodySize: MCP_MAX_REQUEST_BYTES });
const customerNodeHandler = toNodeHandler(customerHandler, { maxRequestBodySize: MCP_MAX_REQUEST_BYTES });
const validateHost = options.allowedHostnames === undefined ? localhostHostValidation() : hostHeaderValidation(options.allowedHostnames);
const validateOrigin = options.allowedOriginHostnames === undefined ? localhostOriginValidation() : originValidation(options.allowedOriginHostnames);

Expand Down
2 changes: 2 additions & 0 deletions services/flo-mcp/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Actor } from "@flo/shared-types";
import { FloError } from "@flo/shared-types";
import { toolOutputSchemas } from "./output-schemas.js";
import { isDemoModeEnabled } from "./runtime-config.js";
import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits.js";

const readOnly: ToolAnnotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
const internalMutation: ToolAnnotations = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
Expand Down Expand Up @@ -48,6 +49,7 @@ export const createFloMcpServer = (orchestrator: FloOrchestrator, actor: Actor,
const server = new McpServer(
{ name: "flo", version: "0.1.0" },
{
maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
instructions: "Flo is a hands-free operations layer. Use tools for all work-order, part, estimate, approval, purchase, and scheduling state. Never assert a transaction succeeded unless confirm_transaction returns success."
}
);
Expand Down
Loading
Loading