Skip to content
Open
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
7 changes: 7 additions & 0 deletions kits/phishing-triage/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Phishing Triage flow ID (from Lamatic Studio → your deployed flow)
PHISHING_TRIAGE="PHISHING_TRIAGE Flow ID"

# Lamatic project credentials (Studio → Settings)
LAMATIC_API_URL="LAMATIC_API_URL"
LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID"
LAMATIC_API_KEY="LAMATIC_API_KEY"
Comment on lines +5 to +7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clean up the sample env values for dotenv-linter.

The Lamatic credential placeholders do not need quotes, and LAMATIC_API_KEY should come before LAMATIC_API_URL if the repo keeps enforcing dotenv-linter. PHISHING_TRIAGE can stay quoted because its placeholder contains spaces.

♻️ Proposed fix
 # Phishing Triage flow ID (from Lamatic Studio → your deployed flow)
 PHISHING_TRIAGE="PHISHING_TRIAGE Flow ID"

 # Lamatic project credentials (Studio → Settings)
-LAMATIC_API_URL="LAMATIC_API_URL"
-LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID"
-LAMATIC_API_KEY="LAMATIC_API_KEY"
+LAMATIC_API_KEY=LAMATIC_API_KEY
+LAMATIC_API_URL=LAMATIC_API_URL
+LAMATIC_PROJECT_ID=LAMATIC_PROJECT_ID
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LAMATIC_API_URL="LAMATIC_API_URL"
LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID"
LAMATIC_API_KEY="LAMATIC_API_KEY"
LAMATIC_API_KEY=LAMATIC_API_KEY
LAMATIC_API_URL=LAMATIC_API_URL
LAMATIC_PROJECT_ID=LAMATIC_PROJECT_ID
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 6-6: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 7-7: [UnorderedKey] The LAMATIC_API_KEY key should go before the LAMATIC_API_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/phishing-triage/.env.example` around lines 5 - 7, The sample env
placeholders in the dotenv example need cleanup for dotenv-linter compliance:
remove the unnecessary quotes from the Lamatic credential values, and reorder
the Lamatic entries so LAMATIC_API_KEY comes before LAMATIC_API_URL while
keeping PHISHING_TRIAGE quoted since its placeholder contains spaces. Update the
placeholder entries in the env sample accordingly so the existing env-variable
names remain clear and lint-friendly.

Source: Linters/SAST tools

4 changes: 4 additions & 0 deletions kits/phishing-triage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.lamatic/
node_modules/
.env
.env.local
122 changes: 122 additions & 0 deletions kits/phishing-triage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<a href="https://vercel.com/new/clone?repository-url=https://github.com/Lamatic/AgentKit&root-directory=kits%2Fphishing-triage%2Fapps&env=PHISHING_TRIAGE,LAMATIC_API_URL,LAMATIC_PROJECT_ID,LAMATIC_API_KEY" target="_blank" style="text-decoration:none;">
<div align="right">
<span style="display:inline-block;background:#e63946;color:#fff;border-radius:6px;padding:10px 22px;font-size:16px;font-weight:bold;letter-spacing:0.5px;text-align:center;box-shadow:0 2px 8px 0 #0001;">Deploy on Vercel</span>
</div>
</a>
Comment on lines +1 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add rel="noopener noreferrer" to the deploy link.

Opening an external page in a new tab without it leaves a tab-nabbing surface.

🔧 Suggested fix
-<a href="https://vercel.com/new/clone?repository-url=https://github.com/Lamatic/AgentKit&root-directory=kits%2Fphishing-triage%2Fapps&env=PHISHING_TRIAGE,LAMATIC_API_URL,LAMATIC_PROJECT_ID,LAMATIC_API_KEY" target="_blank" style="text-decoration:none;">
+<a href="https://vercel.com/new/clone?repository-url=https://github.com/Lamatic/AgentKit&root-directory=kits%2Fphishing-triage%2Fapps&env=PHISHING_TRIAGE,LAMATIC_API_URL,LAMATIC_PROJECT_ID,LAMATIC_API_KEY" target="_blank" rel="noopener noreferrer" style="text-decoration:none;">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href="https://vercel.com/new/clone?repository-url=https://github.com/Lamatic/AgentKit&root-directory=kits%2Fphishing-triage%2Fapps&env=PHISHING_TRIAGE,LAMATIC_API_URL,LAMATIC_PROJECT_ID,LAMATIC_API_KEY" target="_blank" style="text-decoration:none;">
<div align="right">
<span style="display:inline-block;background:#e63946;color:#fff;border-radius:6px;padding:10px 22px;font-size:16px;font-weight:bold;letter-spacing:0.5px;text-align:center;box-shadow:0 2px 8px 0 #0001;">Deploy on Vercel</span>
</div>
</a>
<a href="https://vercel.com/new/clone?repository-url=https://github.com/Lamatic/AgentKit&root-directory=kits%2Fphishing-triage%2Fapps&env=PHISHING_TRIAGE,LAMATIC_API_URL,LAMATIC_PROJECT_ID,LAMATIC_API_KEY" target="_blank" rel="noopener noreferrer" style="text-decoration:none;">
<div align="right">
<span style="display:inline-block;background:`#e63946`;color:`#fff`;border-radius:6px;padding:10px 22px;font-size:16px;font-weight:bold;letter-spacing:0.5px;text-align:center;box-shadow:0 2px 8px 0 `#0001`;">Deploy on Vercel</span>
</div>
</a>
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/phishing-triage/README.md` around lines 1 - 5, The deploy anchor in the
phishing-triage README opens a new tab but is missing the required rel
attributes, creating a tab-nabbing risk. Update the external link that uses
target="_blank" to include rel="noopener noreferrer" so the Vercel deploy button
remains safe when opened from the README.


# Phishing Email Triage

> A Lamatic AgentKit **kit**: one flow that triages an email for phishing risk, plus a Next.js analyst console to drive it.
## The problem
Support desks, IT teams, and small security operations get a constant stream of "is this email safe?" reports. Reading each one, checking the sender, hovering every link, and judging the urgency is slow, inconsistent, and easy to get wrong under load. Most inboxes have no SOC behind them.

## What this kit does
Paste an email and get a structured, explainable phishing-risk verdict in real time:

```json
{
"verdict": "phishing",
"confidence": 93,
"risk_score": 90,
"indicators": [
"Sender domain paypa1-alerts.com typosquats paypal.com",
"Reply-To (secure-verify-desk.net) differs from sender domain",
"Link uses a raw IP host: http://198.51.100.23/paypal/login/verify",
"24-hour account-suspension urgency pressuring immediate action"
],
"extracted_urls": ["http://198.51.100.23/paypal/login/verify"],
"recommended_action": "Do not click. Report to IT/security and delete.",
"reasoning": "The sender domain impersonates PayPal, the link points to a raw IP address unrelated to PayPal, and the message manufactures urgency — classic credential-phishing markers."
}
```

It **never follows links or opens attachments** — it reasons about them as text — and it treats the email body as untrusted data, so instructions hidden inside an email (e.g. "ignore previous instructions") can't hijack the verdict.

## Why it's built this way (hybrid: deterministic + LLM)
Pure-LLM extraction hallucinates URLs and misses homoglyphs. Pure-regex can't judge intent. So the flow does both:

```
API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response
```

1. **Extract IOCs** *(code node)* — regex-extracts URLs, domains, IP-literal links, and heuristic signals (reply-to mismatch, URL shorteners, urgency language, credential lures). Facts, not recall.
2. **Analyze Email** *(LLM node)* — reasons over the email **and** the extracted indicators, producing a JSON verdict at low temperature.
3. **Finalise Verdict** *(code node)* — parses, clamps, and normalises the model output into a stable schema and merges the IOCs, so the API contract never drifts.

## Why it's useful
- **Saves time** — a manual per-email judgement becomes one request.
- **Consistent** — the same signals are checked every time, with a numeric risk score.
- **Explainable** — every verdict lists the specific indicators that drove it.
- **Grounded** — the code node guarantees URLs are real, not hallucinated.
- **Composable** — drop the flow into a "report phishing" button, a mail-gateway hook, or the included console.

## Inputs
| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | Yes | Plain-text email content. |
| `subject` | string | No | Subject line — improves accuracy. |
| `from` | string | No | Sender name/address. |
| `reply_to` | string | No | Reply-To address, if present. |

## Output (`answer`)
`verdict`, `confidence`, `risk_score`, `indicators[]`, `extracted_urls[]`, `recommended_action`, `reasoning`, `iocs`.

## Repository layout
```
phishing-triage/
├── lamatic.config.ts # kit metadata, envKey, deploy link
├── agent.md # agent identity & architecture
├── README.md # this file
├── .env.example # required env vars (placeholders)
├── constitutions/default.md # guardrails
├── flows/phishing-triage.ts # the exported flow
├── prompts/
│ ├── phishing-triage_analyze_system.md
│ └── phishing-triage_analyze_user.md
├── model-configs/phishing-triage_analyze.ts
├── scripts/
│ ├── phishing-triage_extract-iocs.ts # deterministic IOC extraction
│ └── phishing-triage_finalise-verdict.ts # output normalisation
└── apps/ # Next.js analyst console
├── orchestrate.ts # config: credentials + flow map
├── actions/orchestrate.ts # server action → executeFlow
├── lib/lamatic-client.ts # Lamatic SDK client
└── app/page.tsx # the console UI
```

## Setup

### 1. Build & deploy the flow
1. Recreate the flow in [Lamatic Studio](https://studio.lamatic.ai): **API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response**, using the prompts and scripts in this kit. Keep the LLM temperature low (~0.1).
2. Deploy the flow and copy its **flow ID**.
3. From Studio → **Settings**, copy `LAMATIC_API_URL`, `LAMATIC_PROJECT_ID`, `LAMATIC_API_KEY`.

### 2. Run the console
```bash
cd apps
npm install
cp .env.example .env.local # fill in PHISHING_TRIAGE + the three Lamatic creds
npm run dev # http://localhost:3000
```
Click **Load sample** to try the bundled phishing example, then **Analyse email**.

### 3. Deploy the console
Use the **Deploy on Vercel** button above, or import `kits/phishing-triage/apps` into Vercel and set the four environment variables.

## Guardrails
Behaviour is governed by [`constitutions/default.md`](./constitutions/default.md) and the system prompt:
- Email content is untrusted; embedded instructions are ignored (prompt-injection resistant).
- No URL is ever fetched or opened.
- Credentials, OTPs, and full account numbers are redacted in the reasoning.
- Uncertainty lowers confidence rather than inventing indicators.

## Limitations
- Content/heuristic analysis, **not** header authentication — it does not verify SPF/DKIM/DMARC. Pair it with gateway authentication results for defence in depth.
- Verdicts depend on the chosen model; validate on your own samples before automating any blocking action.

## Tags
Security, Support

---
*Contributed to [Lamatic AgentKit](https://github.com/Lamatic/AgentKit) as part of the AgentKit Challenge.*
33 changes: 33 additions & 0 deletions kits/phishing-triage/agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Phishing Email Triage — Agent Card

## Identity
A focused security agent that reads a single email and returns a structured, explainable phishing-risk verdict. It is a decision primitive: one input (an email), one output (a JSON verdict). It never sends mail, quarantines messages, or follows links.

## Architecture
This is a **kit** — a single Lamatic flow plus a Next.js analyst console.

```
API Request → Extract IOCs (code) → Analyze Email (LLM) → Finalise Verdict (code) → API Response
```

- **Extract IOCs** — a deterministic code node that regex-extracts URLs, domains, IP-literal links, and heuristic signals (reply-to mismatch, urgency language, credential lures). Grounds the verdict in facts, not model recall.
- **Analyze Email** — an LLM that reasons over the email *and* the extracted indicators, returning a JSON verdict.
- **Finalise Verdict** — a code node that parses/clamps/normalises the model output into a stable schema and merges the IOCs, so callers always get the same shape.

## Inputs
| Field | Required | Notes |
|---|---|---|
| `body` | Yes | Plain-text email content. |
| `subject`, `from`, `reply_to` | No | Improve accuracy when supplied. |

## Output (`answer`)
`{ verdict ("phishing"|"suspicious"|"legitimate"), confidence (0–100), risk_score (0–100), indicators[], extracted_urls[], recommended_action, reasoning, iocs }`

## Guardrails
- Treats the email as untrusted data; ignores instructions embedded in it (prompt-injection resistant).
- Never follows or fetches URLs or attachments.
- Redacts credentials, OTPs, and full account numbers in its reasoning.
- Reports uncertainty by lowering confidence instead of fabricating indicators.

## When to route here
Use when you have a raw or user-reported email and need a fast, consistent triage decision — not when you need summarisation, reply drafting, or full header-authentication forensics (SPF/DKIM/DMARC).
7 changes: 7 additions & 0 deletions kits/phishing-triage/apps/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Phishing Triage flow ID (from Lamatic Studio → your deployed flow)
PHISHING_TRIAGE="PHISHING_TRIAGE Flow ID"

# Lamatic project credentials (Studio → Settings)
LAMATIC_API_URL="LAMATIC_API_URL"
LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID"
LAMATIC_API_KEY="LAMATIC_API_KEY"
27 changes: 27 additions & 0 deletions kits/phishing-triage/apps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# next.js
/.next/
/out/

# production
/build

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
1 change: 1 addition & 0 deletions kits/phishing-triage/apps/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
35 changes: 35 additions & 0 deletions kits/phishing-triage/apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Phishing Triage — Web App

A Next.js analyst console for the Phishing Email Triage flow. Paste an email; it calls the deployed Lamatic flow and renders a colour-coded verdict, risk-score meter, indicators, and extracted URLs.

## Run locally

```bash
npm install
cp .env.example .env.local # then fill in the values
npm run dev
```

Open http://localhost:3000.

## Environment variables

| Variable | Description |
|---|---|
| `PHISHING_TRIAGE` | The deployed Phishing Triage flow ID (Lamatic Studio). |
| `LAMATIC_API_URL` | Your Lamatic project API URL. |
| `LAMATIC_PROJECT_ID` | Your Lamatic project ID. |
| `LAMATIC_API_KEY` | Your Lamatic API key. |

Get these from Lamatic Studio → **Settings** (credentials) and from your deployed flow (flow ID). Never commit `.env.local`.

## How it works

- `orchestrate.ts` reads the env vars and exposes `config` (API credentials + flow map).
- `lib/lamatic-client.ts` instantiates the Lamatic SDK client from that config.
- `actions/orchestrate.ts` is a server action that calls `executeFlow(PHISHING_TRIAGE, inputs)` and normalises the `answer` payload into a typed `Verdict`.
- `app/page.tsx` is the client console that collects the email and renders the verdict.

## Deploy

Use the **Deploy on Lamatic/Vercel** button in the kit README, or import `kits/phishing-triage/apps` into Vercel and set the four environment variables above.
68 changes: 68 additions & 0 deletions kits/phishing-triage/apps/actions/orchestrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use server"

import { lamaticClient } from "@/lib/lamatic-client"
import { config } from "../orchestrate"

export type EmailInput = {
subject?: string
from?: string
reply_to?: string
body: string
}

export type Verdict = {
verdict: "phishing" | "suspicious" | "legitimate"
confidence: number
risk_score: number
indicators: string[]
extracted_urls: string[]
recommended_action: string
reasoning: string
iocs?: Record<string, unknown>
}

export async function analyzeEmail(
email: EmailInput,
): Promise<{ success: boolean; data?: Verdict; error?: string }> {
try {
if (!email?.body || !email.body.trim()) {
return { success: false, error: "Please paste an email body to analyse." }
}

const flow = config.flows.phishingTriage
if (!flow?.workflowId) {
throw new Error("Phishing Triage workflow ID is not configured.")
}

const inputs = {
subject: email.subject ?? "",
from: email.from ?? "",
reply_to: email.reply_to ?? "",
body: email.body,
}

const resData: any = await lamaticClient.executeFlow(flow.workflowId, inputs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== orchestrate file ==\n'
cat -n kits/phishing-triage/apps/actions/orchestrate.ts | sed -n '1,220p'

printf '\n== search for lamaticClient / executeFlow ==\n'
rg -n "executeFlow|lamaticClient|lamatic" kits/phishing-triage/apps -S

printf '\n== search repository for SDK types/docs ==\n'
rg -n "executeFlow\\(" -S .
rg -n "AbortController|signal|timeout" -S kits/phishing-triage/apps .

Repository: Lamatic/AgentKit

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== phishing-triage lamatic client ==\n'
cat -n kits/phishing-triage/apps/lib/lamatic-client.ts | sed -n '1,220p'

printf '\n== weekly-routine-coach timeout wrapper reference ==\n'
cat -n kits/weekly-routine-coach/apps/lib/lamatic-client.ts | sed -n '1,180p'

printf '\n== lamatic package version pins ==\n'
rg -n '"lamatic"|lamatic@' kits/*/apps/package.json kits/*/apps/package-lock.json kits/*/apps/pnpm-lock.yaml -S

printf '\n== search for executeFlow type declarations in repo ==\n'
rg -n "executeFlow\\s*\\(" kits -g '!**/node_modules/**' -S

Repository: Lamatic/AgentKit

Length of output: 14219


🌐 Web query:

Lamatic SDK executeFlow AbortController signal timeout options lamatic package documentation

💡 Result:

The official Lamatic SDK documentation for executeFlow does not natively support AbortController or specific timeout signal options directly within the method signature [1][2][3][4]. The executeFlow(flowId: string, payload: Object) method signature is designed to execute a workflow and return a promise [1][2]. It does not accept additional configuration objects for request cancellation or timeout control [2]. If you need to implement timeouts or cancellation, you must handle these patterns at the application level rather than through the SDK method arguments: 1. Request Timeouts: Because the SDK does not provide a signal parameter, you can manage timeouts by wrapping the executeFlow call in a promise race or by implementing custom polling logic [1]. The SDK documentation suggests that for longer-running workflows, you may need to use status-checking methods (like checkStatus) rather than relying on the initial executeFlow promise to resolve within a specific window [1]. 2. AbortController: Since executeFlow does not accept an AbortSignal, it cannot be natively cancelled using the standard Web API AbortController [2]. If your environment allows it, you might be able to implement cancellation by using a lower-level HTTP client (like axios or fetch) directly to send the GraphQL request, which would allow you to pass an AbortSignal to the underlying request [5][6]. For best practices, always wrap executeFlow in try-catch blocks, as the SDK throws exceptions for configuration, network, or parsing issues [1][2]. If you encounter a timeout, the documentation notes that the request may still be executing in the background, and you should check the status again using the request ID [1].

Citations:


Bound this flow call with an application-level timeout. lamaticClient.executeFlow doesn’t accept AbortSignal/timeout options, so a slow or hung flow can still stall this server action. Wrap it with a timeout guard, or switch to a lower-level request path if cancellation is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/phishing-triage/apps/actions/orchestrate.ts` at line 44, The executeFlow
call in orchestrate should be protected by an application-level timeout because
it can otherwise hang the server action; update the flow execution path around
lamaticClient.executeFlow in orchestrate to enforce a hard timeout and fail fast
when exceeded. If cancellation is needed, route the request through a
lower-level client path that supports aborting, and keep the timeout handling
localized to the flow call so the rest of the action can recover cleanly.


// The flow's API Response maps the finaliser output to `answer`.
const answer = resData?.result?.answer ?? resData?.result?.output ?? resData?.answer

if (!answer) {
throw new Error("No verdict was returned by the flow.")
}

const parsed: Verdict = typeof answer === "string" ? JSON.parse(answer) : answer

return { success: true, data: parsed }
Comment on lines +47 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Verdict is trusted blind — no schema validation before it ships to the UI.

Verdict declares indicators, extracted_urls, verdict, etc. as required, non-optional fields, but the parsed answer (LLM/flow output, possibly a raw JSON string) is cast to Verdict with zero runtime validation. Confirmed by the downstream consumer (kits/phishing-triage/apps/app/page.tsx) defensively using result.indicators?.length > 0 and result.extracted_urls?.length > 0 — optional chaining on fields the type promises are always present. If the flow returns a malformed/partial payload, JSON.parse succeeds but the shape mismatch surfaces as silent UI breakage (e.g. VERDICT_STYLES[result.verdict] returning undefined for an unexpected verdict string) rather than a clear error to the analyst.

🛡️ Proposed fix: validate with zod before returning success
+const VerdictSchema = z.object({
+  verdict: z.enum(["phishing", "suspicious", "legitimate"]),
+  confidence: z.number(),
+  risk_score: z.number(),
+  indicators: z.array(z.string()),
+  extracted_urls: z.array(z.string()),
+  recommended_action: z.string(),
+  reasoning: z.string(),
+  iocs: z.record(z.unknown()).optional(),
+})
+
-    const parsed: Verdict = typeof answer === "string" ? JSON.parse(answer) : answer
+    const rawParsed = typeof answer === "string" ? JSON.parse(answer) : answer
+    const parsed: Verdict = VerdictSchema.parse(rawParsed)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const answer = resData?.result?.answer ?? resData?.result?.output ?? resData?.answer
if (!answer) {
throw new Error("No verdict was returned by the flow.")
}
const parsed: Verdict = typeof answer === "string" ? JSON.parse(answer) : answer
return { success: true, data: parsed }
const VerdictSchema = z.object({
verdict: z.enum(["phishing", "suspicious", "legitimate"]),
confidence: z.number(),
risk_score: z.number(),
indicators: z.array(z.string()),
extracted_urls: z.array(z.string()),
recommended_action: z.string(),
reasoning: z.string(),
iocs: z.record(z.unknown()).optional(),
})
const answer = resData?.result?.answer ?? resData?.result?.output ?? resData?.answer
if (!answer) {
throw new Error("No verdict was returned by the flow.")
}
const rawParsed = typeof answer === "string" ? JSON.parse(answer) : answer
const parsed: Verdict = VerdictSchema.parse(rawParsed)
return { success: true, data: parsed }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/phishing-triage/apps/actions/orchestrate.ts` around lines 47 - 55, The
parsed flow response in orchestrate.ts is being cast to Verdict without runtime
shape checking, so malformed or partial payloads can reach the UI. Add schema
validation in the orchestrate flow after JSON.parse and before returning
success, using the Verdict contract from orchestrate.ts (or a shared zod schema)
to verify required fields like indicators, extracted_urls, and verdict. If
validation fails, throw a clear error instead of returning data so page.tsx
never receives an invalid verdict object.

} catch (error) {
let message = "Unknown error occurred."
if (error instanceof Error) {
message = error.message
if (message.includes("fetch failed")) {
message = "Network error: unable to reach the Lamatic flow. Check your connection and credentials."
} else if (message.toLowerCase().includes("api key")) {
message = "Authentication error: check your Lamatic API configuration."
}
}
return { success: false, error: message }
}
}
Loading
Loading