code editor: Agent Studio improvement iteration 1 for customer-integration-engineer
This commit is contained in:
@@ -4,5 +4,5 @@
|
|||||||
"repo_head_sha": "fa5803c0bdad221329bdc8410fcb2a193675c938",
|
"repo_head_sha": "fa5803c0bdad221329bdc8410fcb2a193675c938",
|
||||||
"source": "agent-builder",
|
"source": "agent-builder",
|
||||||
"updated_at": 1783944412,
|
"updated_at": 1783944412,
|
||||||
"version": "0.1.5"
|
"version": "0.1.6"
|
||||||
}
|
}
|
||||||
2
a2a.yaml
2
a2a.yaml
@@ -1,5 +1,5 @@
|
|||||||
name: customer-integration-engineer
|
name: customer-integration-engineer
|
||||||
version: 0.1.5
|
version: 0.1.6
|
||||||
entrypoint: agent:CustomerIntegrationEngineer
|
entrypoint: agent:CustomerIntegrationEngineer
|
||||||
expose:
|
expose:
|
||||||
public: false
|
public: false
|
||||||
|
|||||||
26
agent.py
26
agent.py
@@ -410,7 +410,7 @@ class CustomerIntegrationEngineer(A2AAgent[CustomerIntegrationEngineerConfig, No
|
|||||||
"Guides and validates customer integrations from requirements through a tested handoff while "
|
"Guides and validates customer integrations from requirements through a tested handoff while "
|
||||||
"keeping credentials and production changes under customer control."
|
"keeping credentials and production changes under customer control."
|
||||||
)
|
)
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
|
|
||||||
config_model = CustomerIntegrationEngineerConfig
|
config_model = CustomerIntegrationEngineerConfig
|
||||||
auth_model = NoAuth
|
auth_model = NoAuth
|
||||||
@@ -980,24 +980,40 @@ def test_idempotency_key_stable():
|
|||||||
def _typescript_connector(req: IntegrationRequirements, allowlist: list[str], digest: str) -> str:
|
def _typescript_connector(req: IntegrationRequirements, allowlist: list[str], digest: str) -> str:
|
||||||
hosts = json.dumps(allowlist)
|
hosts = json.dumps(allowlist)
|
||||||
return f'''// Minimal connector scaffold for {req.integration_id}. Plan digest: {digest}
|
return f'''// Minimal connector scaffold for {req.integration_id}. Plan digest: {digest}
|
||||||
|
import {{ lookup }} from "node:dns/promises";
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
const ALLOWED_HOSTS = new Set<string>({hosts});
|
const ALLOWED_HOSTS = new Set<string>({hosts});
|
||||||
const MAX_BYTES = {MAX_JSON_BYTES};
|
const MAX_BYTES = {MAX_JSON_BYTES};
|
||||||
|
|
||||||
export function validateUrl(raw: string): URL {{
|
function isPublicAddress(address: string): boolean {{
|
||||||
|
if (net.isIPv4(address)) {{
|
||||||
|
const [a, b] = address.split(".").map(Number);
|
||||||
|
return !(a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168));
|
||||||
|
}}
|
||||||
|
const lower = address.toLowerCase();
|
||||||
|
return net.isIPv6(address) === 6 && !(lower === "::" || lower === "::1" || lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd"));
|
||||||
|
}}
|
||||||
|
|
||||||
|
export async function validateUrl(raw: string): Promise<URL> {{
|
||||||
const url = new URL(raw);
|
const url = new URL(raw);
|
||||||
if (url.protocol !== "https:") throw new Error("connector only permits https URLs");
|
if (url.protocol !== "https:") throw new Error("connector only permits https URLs");
|
||||||
if (!ALLOWED_HOSTS.has(url.hostname.toLowerCase())) throw new Error("host is not allowlisted");
|
const host = url.hostname.toLowerCase();
|
||||||
|
if (!ALLOWED_HOSTS.has(host)) throw new Error("host is not allowlisted");
|
||||||
|
const records = await lookup(host, {{ all: true }});
|
||||||
|
if (records.length === 0 || records.some(record => !isPublicAddress(record.address))) throw new Error("host resolved to unsafe address");
|
||||||
return url;
|
return url;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
export async function sendJson(rawUrl: string, payload: unknown, options: {{ dryRun?: boolean, secretRef?: string }} = {{}}) {{
|
export async function sendJson(rawUrl: string, payload: unknown, options: {{ dryRun?: boolean, secretRef?: string }} = {{}}) {{
|
||||||
const url = validateUrl(rawUrl);
|
const url = await validateUrl(rawUrl);
|
||||||
const body = JSON.stringify(payload);
|
const body = JSON.stringify(payload);
|
||||||
if (new TextEncoder().encode(body).length > MAX_BYTES) throw new Error("payload too large");
|
if (new TextEncoder().encode(body).length > MAX_BYTES) throw new Error("payload too large");
|
||||||
const idem = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
const idem = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
||||||
const idempotencyKey = Array.from(new Uint8Array(idem)).map(b => b.toString(16).padStart(2, "0")).join("");
|
const idempotencyKey = Array.from(new Uint8Array(idem)).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||||
if (options.dryRun !== false) return {{ status: "skipped", reason: "dry_run", idempotencyKey, url: url.toString() }};
|
if (options.dryRun !== false) return {{ status: "skipped", reason: "dry_run", idempotencyKey, url: url.toString() }};
|
||||||
const response = await fetch(url, {{ method: "POST", body, headers: {{ "Content-Type": "application/json", "Idempotency-Key": idempotencyKey }} }});
|
const response = await fetch(url, {{ method: "POST", redirect: "manual", body, headers: {{ "Content-Type": "application/json", "Idempotency-Key": idempotencyKey }} }});
|
||||||
|
if (response.status >= 300 && response.status < 400) throw new Error("redirects are blocked");
|
||||||
return {{ statusCode: response.status, idempotencyKey }};
|
return {{ statusCode: response.status, idempotencyKey }};
|
||||||
}}
|
}}
|
||||||
'''
|
'''
|
||||||
|
|||||||
Reference in New Issue
Block a user