[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f3nl6he0stig0j":3,"$fanuq43nlrv5g":58},{"slug":4,"title":5,"body":6,"summary":7,"tags":8,"author":15,"cover_url":16,"published_at":17,"seo_title":18,"seo_description":19,"reading_minutes":20,"related":21},"palantir-agent-stack-python","Steal Palantir's agent stack: typed tools, one LLM gateway, swappable models","\u003Cp>A thread by \u003Ca href=\"https:\u002F\u002Fx.com\u002FundefinedKi\u002Fstatus\u002F2103883647501652057\">@undefinedKi\u003C\u002Fa> reads Palantir's AI platform docs and pulls out four patterns you can copy into your own project: the agent sees \u003Cstrong>business objects instead of raw data\u003C\u002Fstrong>, every LLM call goes through \u003Cstrong>one gateway\u003C\u002Fstrong>, the \u003Cstrong>model name lives in config\u003C\u002Fstrong>, and agents start from a \u003Cstrong>schedule, an event, or an API call\u003C\u002Fstrong>.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6abe05b0595e0a740c5d2cb8\">\u003Cvideo src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abe05b0595e0a740c5d2cbe-0-3401c9ff.mp4\" autoplay muted loop playsinline preload=\"metadata\">\u003C\u002Fvideo>\u003C\u002Ffigure>\n\n\u003Cp>That's a good list. This post checks each point against Palantir's public docs, so you can see which parts are Palantir's and which are the thread's advice. Then it builds all four in one stdlib-only Python file (about 230 lines) that runs without an API key.\u003C\u002Fp>\n\n\u003Ch2>What Palantir's docs actually say\u003C\u002Fh2>\n\n\u003Ch3>1. Typed business objects and actions, not tables\u003C\u002Fh3>\n\u003Cp>The \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fontology\u002Foverview\">Ontology overview\u003C\u002Fa> says data sources get mapped into \"objects, properties, and links,\" and that change happens through \"action types and functions.\" The \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fagent-studio\u002Ftools\">AIP agent tools page\u003C\u002Fa> lists the tools an agent can be given. These include \u003Cem>Object Query\u003C\u002Fem> (filter, aggregate and traverse links on the object types you pick), \u003Cem>Action\u003C\u002Fem> (make an Ontology edit, optionally only after the user confirms) and \u003Cem>Function\u003C\u002Fem>. You pick which object types and actions each agent can reach, and the model never writes SQL.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6abd88fcc951ea7137fa3878\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88fcc951ea7137fa387d-0-d14f745f.png\" alt=\"A conceptual representation of a business ontology replacing raw data tables.\" loading=\"lazy\">\u003Cfigcaption>A conceptual representation of a business ontology replacing raw data tables.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\n\u003Ch3>2. One governed path to the model\u003C\u002Fh3>\n\u003Cp>This is where you need to be careful about what Palantir actually says. Its docs describe \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fllm-provider-compatible-apis\">LLM-provider-compatible proxy endpoints\u003C\u002Fa> that \"benefit from Foundry capabilities such as rate limiting, data governance, and usage tracking,\" and that enforce zero data retention and georestriction. \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fllm-capacity-management\">LLM capacity management\u003C\u002Fa> sets token-per-minute and request-per-minute limits per enrollment, per project and per user. The \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Faip-security\">AIP security page\u003C\u002Fa> says third-party providers don't retain prompts or completions. For caching, Pipeline Builder's \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fpipeline-builder\u002Fpipeline-builder-llm\">Use LLM node\u003C\u002Fa> can skip rows whose prompt inputs haven't changed since an earlier build.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6abd88fcc951ea7137fa3882\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88fcc951ea7137fa3887-0-9fb7db17.png\" alt=\"A metaphor for a single, governed gateway controlling all access to a model.\" loading=\"lazy\">\u003Cfigcaption>A metaphor for a single, governed gateway controlling all access to a model.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\u003Cp>I couldn't find a Palantir doc that says prompts get \u003Cem>PII-masked\u003C\u002Fem> on their way out, and none that describes an automatic \u003Cem>retry\u003C\u002Fem> policy. Both are the thread's suggestions, and they're good ones. Palantir does sell a \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fsensitive-data-scanner\u002Fcreate-match-conditions\u002Findex.html\">Sensitive Data Scanner\u003C\u002Fa>, but that's a separate product.\u003C\u002Fp>\n\n\u003Ch3>3. Swappable models\u003C\u002Fh3>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fsupported-llms\">Supported LLMs\u003C\u002Fa> covers models from xAI, OpenAI, Anthropic, Meta and Google. \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fbring-your-own-model\">Bring your own model\u003C\u002Fa> lets you register an externally hosted or self-hosted LLM for use in AIP Logic, Chatbot Studio and other apps. In the thread's words, keep the model name in config, and switching becomes a one-line change.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6abd88fcc951ea7137fa388c\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88fcc951ea7137fa3891-0-fc02af54.png\" alt=\"An illustration of swappable model components in a modular system.\" loading=\"lazy\">\u003Cfigcaption>An illustration of swappable model components in a modular system.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\n\u003Ch3>4. Schedule, event, API\u003C\u002Fh3>\n\u003Cp>Automate supports \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fautomate\u002Fcondition-time\u002Findex.html\">time conditions\u003C\u002Fa> (hourly, daily, weekly, monthly, or custom cron) and \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fautomate\u002Fcondition-objects\">object set conditions\u003C\u002Fa> (objects added, removed or modified, threshold crossed). An automation can call a Logic function \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Flogic\u002Faip-logic-integration-automate\">for each new object\u003C\u002Fa>. For the API side, the \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Flogic\u002Fgetting-started\">Logic getting-started guide\u003C\u002Fa> shows copying a curl command to run a function from outside Foundry (not for Logics that return Ontology edits), and published queries can be called through the \u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fapi\u002Fontologies-v2-resources\u002Fqueries\u002Fexecute-query\">Execute Query API\u003C\u002Fa>.\u003C\u002Fp>\n\n\u003Ch2>The minimal version\u003C\u002Fh2>\n\n\u003Cp>Save this as \u003Ccode>agent_stack.py\u003C\u002Fcode>. It needs Python 3.10+ and nothing else: no pip install, no key. SQLite stands in for your database, a fake provider stands in for the LLM, and \u003Ccode>MODEL\u003C\u002Fcode> picks the provider.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>\"\"\"agent_stack.py - a minimal \"ontology + gateway + triggers\" agent stack.\n\nStdlib only (Python 3.10+). Run:\n    MODEL=fake python3 agent_stack.py          # event + schedule demo\n    MODEL=fake python3 agent_stack.py serve    # API trigger on :8765\n\"\"\"\nimport hashlib, json, os, re, sched, sqlite3, sys, time, urllib.error, urllib.request\nfrom dataclasses import dataclass, asdict\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\n# ---------------------------------------------------------------- 1. data\nDB = sqlite3.connect(\":memory:\", check_same_thread=False)\nDB.executescript(\"\"\"\nCREATE TABLE customers(id TEXT PRIMARY KEY, name TEXT, email TEXT, phone TEXT, tier TEXT);\nCREATE TABLE orders(id TEXT PRIMARY KEY, customer_id TEXT, total REAL, status TEXT);\nINSERT INTO customers VALUES ('c1','Ada Lovelace','ada@example.com','+44 20 7946 0958','gold');\nINSERT INTO orders VALUES ('o1','c1',120.0,'new'), ('o2','c1',4800.0,'new');\n\"\"\")\n\n# ---------------------------------------------------------------- 2. business objects\n@dataclass(frozen=True)\nclass Customer:\n    id: str\n    name: str\n    email: str\n    phone: str\n    tier: str\n\n@dataclass(frozen=True)\nclass Order:\n    id: str\n    customer_id: str\n    total: float\n    status: str\n\n# ---------------------------------------------------------------- 3. typed tools\nTOOLS = {}\n\ndef tool(fn):\n    \"\"\"Register a function as an agent tool. The LLM only ever sees these.\"\"\"\n    TOOLS[fn.__name__] = fn\n    return fn\n\n@tool\ndef get_order(order_id: str) -&gt; Order:\n    row = DB.execute(\"SELECT id, customer_id, total, status FROM orders WHERE id=?\",\n                     (order_id,)).fetchone()\n    if row is None:\n        raise LookupError(f\"order {order_id} not found\")\n    return Order(*row)\n\n@tool\ndef get_customer(customer_id: str) -&gt; Customer:\n    row = DB.execute(\"SELECT id, name, email, phone, tier FROM customers WHERE id=?\",\n                     (customer_id,)).fetchone()\n    if row is None:\n        raise LookupError(f\"customer {customer_id} not found\")\n    return Customer(*row)\n\n@tool\ndef flag_order_for_review(order_id: str, reason: str) -&gt; dict:\n    \"\"\"An *action*: the only write the agent is allowed to make.\"\"\"\n    if not reason.strip():\n        raise ValueError(\"reason is required\")\n    DB.execute(\"UPDATE orders SET status='review' WHERE id=? AND status='new'\", (order_id,))\n    return {\"order_id\": order_id, \"status\": \"review\", \"reason\": reason}\n\ndef call_tool(name: str, args: dict) -&gt; dict:\n    if name not in TOOLS:\n        raise PermissionError(f\"tool {name!r} is not on the allow-list\")\n    result = TOOLS[name](**args)\n    return asdict(result) if hasattr(result, \"__dataclass_fields__\") else result\n\ndef tool_catalog() -&gt; str:\n    return \"\\n\".join(f\"- {n}{TOOLS[n].__annotations__}\" for n in TOOLS)\n\n# ---------------------------------------------------------------- 4. providers\nclass TransientError(Exception):\n    \"\"\"Timeouts, 429s, 5xx: worth retrying.\"\"\"\n\nclass FakeProvider:\n    \"\"\"Deterministic stand-in so the demo runs with no API key.\n    Hard-coded to this file's prompt format (it looks for TOOL\u002Forder\n    lines); a real provider replaces it, not extends it.\n    Fails its first call to show the retry path.\"\"\"\n    def __init__(self):\n        self.calls = 0\n        self.last_prompt = \"\"\n\n    def complete(self, model: str, prompt: str) -&gt; str:\n        self.calls += 1\n        self.last_prompt = prompt\n        if self.calls == 1:\n            raise TransientError(\"simulated 503\")\n        seen = re.findall(r\"TOOL (\\w+) -&gt; (.*)\", prompt)\n        done = {name: json.loads(body) for name, body in seen}\n        task = re.search(r\"order (\\w+)\", prompt).group(1)\n        if \"get_order\" not in done:\n            return json.dumps({\"tool\": \"get_order\", \"args\": {\"order_id\": task}})\n        order = done[\"get_order\"]\n        if \"get_customer\" not in done:\n            return json.dumps({\"tool\": \"get_customer\",\n                               \"args\": {\"customer_id\": order[\"customer_id\"]}})\n        if order[\"total\"] &gt; 1000 and \"flag_order_for_review\" not in done:\n            return json.dumps({\"tool\": \"flag_order_for_review\",\n                               \"args\": {\"order_id\": order[\"id\"],\n                                        \"reason\": \"total above 1000\"}})\n        return json.dumps({\"answer\": f\"order {order['id']} checked, total {order['total']}\"})\n\nclass OpenAICompatibleProvider:\n    \"\"\"Any \u002Fv1\u002Fchat\u002Fcompletions endpoint (OpenAI, OVH, vLLM, Ollama, LiteLLM proxy...).\"\"\"\n    def __init__(self):\n        self.base = os.environ.get(\"LLM_BASE_URL\", \"https:\u002F\u002Fapi.openai.com\u002Fv1\")\n        self.key = os.environ[\"LLM_API_KEY\"]\n\n    def complete(self, model: str, prompt: str) -&gt; str:\n        req = urllib.request.Request(\n            f\"{self.base}\u002Fchat\u002Fcompletions\",\n            data=json.dumps({\"model\": model,\n                             \"messages\": [{\"role\": \"user\", \"content\": prompt}]}).encode(),\n            headers={\"Authorization\": f\"Bearer {self.key}\",\n                     \"Content-Type\": \"application\u002Fjson\"})\n        try:\n            with urllib.request.urlopen(req, timeout=60) as r:\n                return json.load(r)[\"choices\"][0][\"message\"][\"content\"]\n        except urllib.error.HTTPError as e:\n            if e.code == 429 or e.code &gt;= 500:\n                raise TransientError(str(e)) from e\n            raise\n        except (urllib.error.URLError, TimeoutError) as e:    # timeouts, refused connections\n            raise TransientError(str(e)) from e\n\n# ---------------------------------------------------------------- 5. the gateway\nEMAIL = re.compile(r\"[\\w.+-]+@[\\w-]+\\.[\\w.-]+\")\nPHONE = re.compile(r\"\\+?\\d[\\d\\s().-]{7,}\\d\")\n\ndef mask_pii(text: str) -&gt; str:\n    return PHONE.sub(\"[PHONE]\", EMAIL.sub(\"[EMAIL]\", text))\n\nclass Gateway:\n    \"\"\"Every LLM call in the app goes through here - nowhere else.\"\"\"\n    def __init__(self):\n        spec = os.environ.get(\"MODEL\", \"fake\")           # e.g. \"fake\" or \"openai:gpt-4o-mini\"\n        provider, _, self.model = spec.partition(\":\")\n        self.provider = {\"fake\": FakeProvider,\n                         \"openai\": OpenAICompatibleProvider}[provider]()\n        self.max_retries = int(os.environ.get(\"LLM_MAX_RETRIES\", \"3\"))\n        self.cache: dict[str, str] = {}\n        self.stats = {\"calls\": 0, \"cache_hits\": 0, \"retries\": 0}\n\n    def complete(self, prompt: str) -&gt; str:\n        prompt = mask_pii(prompt)                          # mask before it leaves\n        key = hashlib.sha256(f\"{self.model}\\x00{prompt}\".encode()).hexdigest()\n        if key in self.cache:\n            self.stats[\"cache_hits\"] += 1\n            return self.cache[key]\n        for attempt in range(self.max_retries + 1):\n            try:\n                self.stats[\"calls\"] += 1\n                out = self.provider.complete(self.model, prompt)\n                self.cache[key] = out\n                return out\n            except TransientError:\n                if attempt == self.max_retries:\n                    raise\n                self.stats[\"retries\"] += 1\n                time.sleep(0.1 * 2 ** attempt)             # 0.1s, 0.2s, 0.4s ...\n        raise RuntimeError(\"unreachable\")\n\nGATEWAY = Gateway()\n\n# ---------------------------------------------------------------- 6. the agent loop\ndef run_agent(task: str, max_steps: int = 6) -&gt; str:\n    transcript = [f\"TASK {task}\", \"Reply with JSON: {\\\"tool\\\",\\\"args\\\"} or {\\\"answer\\\"}.\",\n                  \"TOOLS:\\n\" + tool_catalog()]\n    for _ in range(max_steps):\n        reply = json.loads(GATEWAY.complete(\"\\n\".join(transcript)))\n        if \"answer\" in reply:\n            return reply[\"answer\"]\n        result = call_tool(reply[\"tool\"], reply.get(\"args\", {}))\n        transcript.append(f\"TOOL {reply['tool']} -&gt; {json.dumps(result)}\")\n    raise RuntimeError(\"agent did not finish\")\n\n# ---------------------------------------------------------------- 7. triggers\nSUBSCRIBERS: dict[str, list] = {}\n\ndef on_event(name):\n    def register(fn):\n        SUBSCRIBERS.setdefault(name, []).append(fn)\n        return fn\n    return register\n\ndef publish(name, payload):\n    for fn in SUBSCRIBERS.get(name, []):\n        fn(payload)\n\n@on_event(\"order.created\")\ndef review_new_order(payload):\n    print(\"[event]   \", run_agent(f\"review order {payload['order_id']}\"))\n\ndef nightly_sweep():\n    for (oid,) in DB.execute(\"SELECT id FROM orders WHERE status='new'\").fetchall():\n        print(\"[schedule]\", run_agent(f\"review order {oid}\"))\n\nclass API(BaseHTTPRequestHandler):\n    def do_POST(self):\n        try:\n            length = int(self.headers.get(\"Content-Length\") or 0)\n            body = json.loads(self.rfile.read(length) or b\"{}\")\n            status, out = 200, {\"answer\": run_agent(f\"review order {body['order_id']}\")}\n        except (ValueError, KeyError, TypeError, LookupError) as e:\n            status, out = 400, {\"error\": str(e)}\n        self.send_response(status)\n        self.send_header(\"Content-Type\", \"application\u002Fjson\")\n        self.end_headers()\n        self.wfile.write(json.dumps(out).encode())\n\nif __name__ == \"__main__\":\n    if len(sys.argv) &gt; 1 and sys.argv[1] == \"serve\":\n        print(\"POST http:\u002F\u002F127.0.0.1:8765  {\\\"order_id\\\": \\\"o1\\\"}\")\n        HTTPServer((\"127.0.0.1\", 8765), API).serve_forever()\n        sys.exit(0)\n    publish(\"order.created\", {\"order_id\": \"o2\"})          # event trigger\n    s = sched.scheduler(time.monotonic, time.sleep)      # schedule trigger\n    s.enter(0.2, 1, nightly_sweep)                       # in prod: cron \u002F APScheduler\n    s.run()                                              # blocks until the queued job has run\n    publish(\"order.created\", {\"order_id\": \"o2\"})          # same task again -&gt; cache\n    print(\"statuses:\", DB.execute(\"SELECT id, status FROM orders\").fetchall())\n    print(\"gateway: \", GATEWAY.stats)\n    print(\"provider saw:\", [l for l in GATEWAY.provider.last_prompt.splitlines()\n                            if \"get_customer\" in l and \"TOOL\" in l])\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2>Run it\u003C\u002Fh2>\n\n\u003Cpre>\u003Ccode>MODEL=fake python3 agent_stack.py\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>MODEL\u003C\u002Fcode> defaults to \u003Ccode>fake\u003C\u002Fcode>, so plain \u003Ccode>python3 agent_stack.py\u003C\u002Fcode> works too (on Windows, where the \u003Ccode>VAR=value command\u003C\u002Fcode> prefix isn't supported, use that or \u003Ccode>set MODEL=fake\u003C\u002Fcode> first). Output on my machine (Python 3.14):\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>[event]    order o2 checked, total 4800.0\n[schedule] order o1 checked, total 120.0\n[event]    order o2 checked, total 4800.0\nstatuses: [('o1', 'new'), ('o2', 'review')]\ngateway:  {'calls': 11, 'cache_hits': 1, 'retries': 1}\nprovider saw: ['TOOL get_customer -&gt; {\"id\": \"c1\", \"name\": \"Ada Lovelace\", \"email\": \"[EMAIL]\", \"phone\": \"[PHONE]\", \"tier\": \"gold\"}']\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Each line shows one of the patterns working:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Typed tools:\u003C\u002Fstrong> the agent read an \u003Ccode>Order\u003C\u002Fcode> and a \u003Ccode>Customer\u003C\u002Fcode> and moved \u003Ccode>o2\u003C\u002Fcode> into review through the one write action it's allowed. It never saw a table name or a SQL string.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>PII masking:\u003C\u002Fstrong> the \u003Ccode>provider saw\u003C\u002Fcode> line is the exact tool result the model received. The email and phone number were replaced before the prompt left the process.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Retry:\u003C\u002Fstrong> the fake provider fails its first call with a simulated 503. The gateway backed off, tried again, and counted it (\u003Ccode>'retries': 1\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Cache:\u003C\u002Fstrong> the second \u003Ccode>order.created\u003C\u002Fcode> event started with the same prompt as the first, so that step came from the cache (\u003Ccode>'cache_hits': 1\u003C\u002Fcode>). Later steps missed because \u003Ccode>o2\u003C\u002Fcode>'s status had changed, and that's what you want. The cache key includes the model name.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Triggers:\u003C\u002Fstrong> the same \u003Ccode>run_agent\u003C\u002Fcode> function ran from an event and from a scheduled sweep. The sweep skipped \u003Ccode>o2\u003C\u002Fcode> because it was no longer \u003Ccode>new\u003C\u002Fcode>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>For the API trigger, start the server and POST to it:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>MODEL=fake python3 agent_stack.py serve\n# in another terminal\ncurl -s -X POST localhost:8765 -d '{\"order_id\":\"o1\"}'\n# {\"answer\": \"order o1 checked, total 120.0\"}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2>Plug in a real model\u003C\u002Fh2>\n\n\u003Cp>The \u003Ccode>OpenAICompatibleProvider\u003C\u002Fcode> class talks to any \u003Ccode>\u002Fchat\u002Fcompletions\u003C\u002Fcode> endpoint: OpenAI, a LiteLLM proxy, vLLM, or Ollama's OpenAI-compatible API. Switching is an environment change:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode>export MODEL=openai:gpt-4o-mini\nexport LLM_API_KEY=...          # your key\nexport LLM_BASE_URL=https:\u002F\u002Fapi.openai.com\u002Fv1   # or http:\u002F\u002Flocalhost:11434\u002Fv1 for Ollama\npython3 agent_stack.py\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>This path wasn't run for this post (no key was used), and a real model may not follow the JSON reply format as closely as the fake one does. Before you ship it, add a parse-and-retry step, or use your provider's native tool-calling or JSON mode. To add another provider, write a class with \u003Ccode>complete(model, prompt) -&gt; str\u003C\u002Fcode>, raise \u003Ccode>TransientError\u003C\u002Fcode> on 429s and 5xx, and add it to the dict in \u003Ccode>Gateway.__init__\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Ch2>What to harden before production\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Masking:\u003C\u002Fstrong> two regexes miss names, addresses and IDs. Better options are to drop fields in the tool layer (the model rarely needs \u003Ccode>email\u003C\u002Fcode>) or to use a dedicated detector such as Microsoft Presidio.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Cache:\u003C\u002Fstrong> an in-process dict disappears on restart and isn't shared between workers. Move it to Redis with a TTL, and only cache calls whose answers can safely repeat.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Actions:\u003C\u002Fstrong> the Palantir pattern of asking the user to confirm before an edit is easy to copy. Have write tools return a pending change that a person approves.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Accounting:\u003C\u002Fstrong> the gateway already sees every call, so it's where token counts, per-user limits and an audit log should go. That matches what the Palantir docs describe for their proxy endpoints.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Triggers:\u003C\u002Fstrong> swap \u003Ccode>sched\u003C\u002Fcode> for cron or APScheduler, the in-memory bus for Kafka, NATS or Redis Streams, and \u003Ccode>http.server\u003C\u002Fcode> for your web framework. \u003Ccode>run_agent\u003C\u002Fcode> doesn't need to change.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The shape itself is what matters: a short list of typed tools, one function that owns every model call, and a config value that decides which model answers.\u003C\u002Fp>\n\n\u003Ch2>Sources\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fx.com\u002FundefinedKi\u002Fstatus\u002F2103883647501652057\">@undefinedKi on X: Palantir agent-stack thread\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fontology\u002Foverview\">Palantir: Ontology overview\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fagent-studio\u002Ftools\">Palantir: AIP Agent Studio tools\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Flogic\u002Foverview\">Palantir: AIP Logic overview\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Flogic\u002Fgetting-started\">Palantir: AIP Logic getting started\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fllm-provider-compatible-apis\">Palantir: LLM-provider compatible APIs\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fllm-capacity-management\">Palantir: LLM capacity management\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Faip-security\">Palantir: AIP security and privacy\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fpipeline-builder\u002Fpipeline-builder-llm\">Palantir: Pipeline Builder Use LLM node\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fsensitive-data-scanner\u002Fcreate-match-conditions\u002Findex.html\">Palantir: Sensitive Data Scanner match conditions\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fsupported-llms\">Palantir: Supported LLMs\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Faip\u002Fbring-your-own-model\">Palantir: Bring your own model\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fautomate\u002Fcondition-time\u002Findex.html\">Palantir: Automate time condition\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fautomate\u002Fcondition-objects\">Palantir: Automate object set conditions\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Flogic\u002Faip-logic-integration-automate\">Palantir: Automate AIP Logic\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.palantir.com\u002Fdocs\u002Ffoundry\u002Fapi\u002Fontologies-v2-resources\u002Fqueries\u002Fexecute-query\">Palantir: Execute Query API\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>","An X thread boils Palantir's AIP docs down to four agent patterns. We check each one against the docs, then build them in one stdlib-only Python file: typed business-object tools, a gateway that masks PII, caches and retries, a model set in config, and schedule\u002Fevent\u002FAPI triggers.",[9,10,11,12,13,14],"ai-agents","llm","python","architecture","palantir","ai-assisted","if.codes","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88f9c951ea7137fa3872-0-48eb7eee.png","2026-10-01T23:05:10.531Z","Palantir's AI agent stack in one Python file","Typed business-object tools, one LLM gateway (PII masking, cache, retries), config-swappable models and triggers, in a runnable stdlib-only Python file.",10,[22,34,46],{"slug":23,"title":24,"type":25,"summary":26,"tags":27,"author":15,"cover_url":31,"published_at":32,"updated_at":33},"claude-code-effort-levels","Effort levels in Claude Code: when max effort pays off and when it just burns tokens","blog","Anthropic's effort deep dive (Terminal-Bench 3.0 plus three builds) shows higher effort mostly buys verification and edge-case testing, not smarter code. A rule of thumb per task type, the commands to set effort, and a script to measure cost vs pass rate on your own repo.",[28,29,10,30,14],"claude-code","ai-coding","developer-tools","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88edc951ea7137fa3804-0-2716005a.png","2026-10-01T22:32:14.139Z","2026-10-01T22:32:14.14Z",{"slug":35,"title":36,"type":25,"summary":37,"tags":38,"author":15,"cover_url":43,"published_at":44,"updated_at":45},"agentic-inbox-cloudflare-setup","Self-host an AI email agent on Cloudflare Workers: agentic-inbox set up and costed","Cloudflare's open-source agentic-inbox runs a full email client on Workers, with one SQLite Durable Object per mailbox and a Kimi K2.5 agent that drafts replies. Covers the post-deploy steps people miss (Access, sending, routing, mailbox first) and the cost.",[39,40,9,41,42,14],"cloudflare","workers","email","self-hosting","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88dac951ea7137fa378b-0-5b0ec96d.png","2026-10-01T08:17:18.646Z","2026-10-01T08:44:42.528Z",{"slug":47,"title":48,"type":25,"summary":49,"tags":50,"author":15,"cover_url":55,"published_at":56,"updated_at":57},"audit-ai-agent-public-traces","Nearly a million leaked links: auditing what your AI agents leave on the public web","OpenAI's agent swarm left almost a million public shortener URLs holding credentials. Here's a tested shell + gitleaks audit to find the shortlinks, pastes and webhooks your own agents created, scan them for secrets and close the channels.",[51,52,53,54,10,14],"security","agents","secrets","gitleaks","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abaad873e21d5cbd14d4397-0-0f4f51f7.png","2026-10-01T07:43:19.642Z","2026-10-01T08:44:43.041Z",[59,62,65,68,70,81,91,104,116,125,136,147,156,167],{"slug":4,"title":5,"type":25,"summary":7,"tags":60,"author":15,"cover_url":16,"published_at":17,"updated_at":61,"reading_minutes":20},[9,10,11,12,13,14],"2026-10-01T23:05:10.532Z",{"slug":23,"title":24,"type":25,"summary":26,"tags":63,"author":15,"cover_url":31,"published_at":32,"updated_at":33,"reading_minutes":64},[28,29,10,30,14],9,{"slug":35,"title":36,"type":25,"summary":37,"tags":66,"author":15,"cover_url":43,"published_at":44,"updated_at":45,"reading_minutes":67},[39,40,9,41,42,14],8,{"slug":47,"title":48,"type":25,"summary":49,"tags":69,"author":15,"cover_url":55,"published_at":56,"updated_at":57,"reading_minutes":67},[51,52,53,54,10,14],{"slug":71,"title":72,"type":25,"summary":73,"tags":74,"author":15,"cover_url":78,"published_at":79,"updated_at":80,"reading_minutes":67},"mikrotrick-check-patch-mikrotik","MikroTrick: check and patch your MikroTik in 15 minutes","Two chained RouterOS bugs give anyone who can reach SSH full admin, no password needed, and attacks started before the patch. Find exposed SSH, check the version, grep for the published IoCs, patch and move management behind WireGuard.",[51,75,76,77,42,14],"mikrotik","routeros","ssh","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abcd9c0838b650cb96b3d10-0-6cd107e0.png","2026-10-01T07:02:15.03Z","2026-10-01T08:44:41.391Z",{"slug":82,"title":83,"type":25,"summary":84,"tags":85,"author":15,"cover_url":88,"published_at":89,"updated_at":90,"reading_minutes":67},"agent-sandbox-dns-egress-lockdown","Your agent sandbox leaks through DNS: lock down egress in 15 minutes","An OpenAI model escaped its sandbox by tunnelling questions through DNS. Here is a tested Docker Compose setup for coding agents: a DNS allowlist, a logging egress proxy and a kill switch that actually fires.",[51,86,52,87,42,14],"docker","dns","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abaaca13e21d5cbd14d4306-0-cb93fe1b.png","2026-10-01T03:00:15.943Z","2026-10-01T08:44:43.165Z",{"slug":92,"title":93,"type":25,"summary":94,"tags":95,"author":15,"cover_url":100,"published_at":101,"updated_at":102,"reading_minutes":103},"who-blocks-ai-crawlers-robots-txt","Who blocks AI crawlers? robots.txt vs the network edge, with numbers","I scanned robots.txt on the top 300 sites: 33 of 138 block GPTBot, 14 block training but allow AI search. What each AI bot directive controls, why robots.txt is only a request, and a copy-paste policy plus nginx rule for small SaaS sites.",[96,97,98,39,99,14],"ai","robots-txt","seo","saas","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abcd9c1838b650cb96b3d1b-0-11f94198.png","2026-09-30T21:00:20.673Z","2026-10-01T20:47:34.092Z",7,{"slug":105,"title":106,"type":25,"summary":107,"tags":108,"author":15,"cover_url":112,"published_at":113,"updated_at":114,"reading_minutes":115},"bullet-time-with-first-last-frame-video","Bullet time with first\u002Flast-frame video: orbiting a frozen moment from three stills","A freeze-frame camera orbit built from generated stills: one action shot, two camera-move angles, two first\u002Flast-frame clips between them, stitched and ping-ponged. The pipeline, the seams, and where the model re-imagines the water.",[96,109,110,111],"comfyui","video-generation","flowdsl","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abae46c45201648bfd477a7-0-6f4d036e.png","2026-09-28T22:42:41Z","2026-09-28T22:42:41.6Z",4,{"slug":117,"title":118,"type":25,"summary":119,"tags":120,"author":15,"cover_url":122,"published_at":123,"updated_at":124,"reading_minutes":103},"an-ai-media-pipeline-that-shows-its-work","An AI media pipeline that shows its work: ComfyUI presets, FlowDSL routing and the misses","How the images on my sites are generated: four ComfyUI presets behind one Go module, job rows as state, FlowDSL flows for routing, per-post media in the admin — and the bugs and model misses I hit shipping it. This post's own images were made the same way.",[96,111,109,121],"image-generation","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba8ba317ceba3543925be4-0-2f6b8a5c.png","2026-09-28T15:57:50Z","2026-10-01T20:47:34.327Z",{"slug":126,"title":127,"type":25,"summary":128,"tags":129,"author":15,"cover_url":132,"published_at":133,"updated_at":134,"reading_minutes":135},"openai-embeddings-python-mongodb","Transforming Text into Vectors: OpenAI Embeddings in Python","Learn how to generate text embeddings with the OpenAI API in Python to power semantic search, recommendations, and more. Includes practical examples with MongoDB integration and cost analysis.",[130,96,11,131],"openai","mongodb","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c2d-0-2e60b732.png","2024-11-23T00:00:00Z","2026-09-28T22:31:01.385Z",3,{"slug":137,"title":138,"type":25,"summary":139,"tags":140,"author":15,"cover_url":143,"published_at":144,"updated_at":145,"reading_minutes":146},"check-pricing-availability-ing-domains","Last Chance to Grab Short .ING Domains: The Extended List Part II","Welcome back to the second part of our exciting exploration into the .ING domain zone! This time, I've expanded our horizons to bring you an even larger selection of .ING domain names. List of over 24,000 domain names inside.",[141,142],"domains","business","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c38-0-c55f4c8d.png","2023-12-14T00:00:00Z","2026-09-28T22:31:01.453Z",1,{"slug":148,"title":149,"type":25,"summary":150,"tags":151,"author":15,"cover_url":152,"published_at":153,"updated_at":154,"reading_minutes":155},"impressive-ing-domains","Unveiling the Impressive .ING Domains","Discover the vast potential of the new .ING domain zone in my latest blog post! I've used AI and a Python script to unearth a treasure trove of available domain names. From budget-friendly picks to exclusive premium domains, there's something for every ambition. Plus, a special list of unique, lesser-known domains awaits.",[141,142],"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c43-0-03be24b7.png","2023-12-11T00:00:00Z","2026-09-28T22:31:01.527Z",2,{"slug":157,"title":158,"type":25,"summary":159,"tags":160,"author":15,"cover_url":164,"published_at":165,"updated_at":166,"reading_minutes":135},"secured-web-server-in-5-minutes","Fortify Web Server Security in 5 Minutes with Tailscale","Tailscale revolutionizes secure networking with its user-friendly approach, effortlessly connecting devices across diverse networks.",[161,162,163],"firewall","tailscale","webserver","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c4e-0-eae6f62d.png","2023-11-03T00:00:00Z","2026-09-28T22:31:01.597Z",{"slug":168,"title":169,"type":25,"summary":170,"tags":171,"author":15,"cover_url":174,"published_at":175,"updated_at":176,"reading_minutes":177},"lets-encrypt-free-ssl","How to Secure Your Website with Free SSL Certificates for a Lifetime","Let’s Encrypt certificates have revolutionized internet security by providing free, automated, and widely trusted SSL\u002FTLS certificates. The non-profit Certificate Authority (CA) has significantly contributed to a more secure web environment by simplifying the process of securing websites with HTTPS.",[172,173,163],"ssl","https","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c59-0-bf2a9a0a.png","2023-11-01T00:00:00Z","2026-09-28T22:39:13.555Z",6]