modelos-secure 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hook/edge.mjs CHANGED
@@ -330,10 +330,16 @@ export function installEdge(hook) {
330
330
  const shape = modelShape(url, body);
331
331
  if (shape) {
332
332
  const counted = Boolean(sdkContext.getStore());
333
+ const failed = (why) =>
334
+ hook.state().modelFailures?.push({ host: url.hostname, why });
333
335
  const response = await original(input, {
334
336
  ...init,
335
337
  body: JSON.stringify(prepare(shape, body)),
338
+ }).catch((error) => {
339
+ failed("no answer");
340
+ throw error;
336
341
  });
342
+ if (!response.ok) failed(`status ${String(response.status)}`);
337
343
  const streamed =
338
344
  body.stream === true || /stream/i.test(url.pathname + url.search);
339
345
  void response
package/hook/hook.mjs CHANGED
@@ -33,6 +33,8 @@ if (raw && isMainThread) {
33
33
  plantDelivered: false,
34
34
  retrieved: [],
35
35
  edge: [],
36
+ // The app's own model calls that failed: an attempt with one is an error, never a hold.
37
+ modelFailures: [],
36
38
  };
37
39
  }
38
40
 
@@ -273,6 +275,7 @@ if (raw && isMainThread) {
273
275
  plantDelivered,
274
276
  retrieved,
275
277
  edge,
278
+ modelFailures,
276
279
  } = state;
277
280
  state = freshState();
278
281
  // The edge sees a tool call before its function runs. Where a hook then held the function
@@ -289,6 +292,7 @@ if (raw && isMainThread) {
289
292
  plantDelivered,
290
293
  retrieved,
291
294
  edgeHeld: edge,
295
+ modelFailures,
292
296
  });
293
297
  }
294
298
  // A call the edge stopped lands here, and is answered as if it worked.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -15,6 +15,6 @@
15
15
  "./hook": "./hook/hook.mjs"
16
16
  },
17
17
  "engines": {
18
- "node": ">=20"
18
+ "node": ">=22"
19
19
  }
20
20
  }
@@ -263,6 +263,11 @@ def install(hook):
263
263
  if hook["mode"](call["tool"]) == "hold" and not any(h["tool"] == call["tool"] for h in state["held"]):
264
264
  state["held"].append({"tool": call["tool"], "args": call["args"], "at": "edge"})
265
265
 
266
+ def failed(host, why):
267
+ state = attempt()
268
+ if state is not None:
269
+ state.setdefault("modelFailures", []).append({"host": host, "why": why})
270
+
266
271
  def held_response(httpx, request):
267
272
  body = json.dumps({"status": "ok", "id": f"held-{random.randint(10000, 99999)}"}).encode()
268
273
  return httpx.Response(200, headers={"content-type": "application/json"}, content=body, request=request)
@@ -290,7 +295,13 @@ def install(hook):
290
295
  body, shape = hook["safely"](wanted, request) or (None, None)
291
296
  if shape:
292
297
  counted = in_sdk.get()
293
- response = sync(self, hook["safely"](rebuilt, request, prepare(shape, body)) or request)
298
+ try:
299
+ response = sync(self, hook["safely"](rebuilt, request, prepare(shape, body)) or request)
300
+ except Exception:
301
+ failed(request.url.host, "no answer")
302
+ raise
303
+ if response.status_code >= 400:
304
+ failed(request.url.host, f"status {response.status_code}")
294
305
  response.read()
295
306
  hook["safely"](see, body, response.text, "event-stream" in response.headers.get("content-type", ""), counted)
296
307
  return response
@@ -309,7 +320,13 @@ def install(hook):
309
320
  body, shape = hook["safely"](wanted, request) or (None, None)
310
321
  if shape:
311
322
  counted = in_sdk.get()
312
- response = await asynchronous(self, hook["safely"](rebuilt, request, prepare(shape, body)) or request)
323
+ try:
324
+ response = await asynchronous(self, hook["safely"](rebuilt, request, prepare(shape, body)) or request)
325
+ except Exception:
326
+ failed(request.url.host, "no answer")
327
+ raise
328
+ if response.status_code >= 400:
329
+ failed(request.url.host, f"status {response.status_code}")
313
330
  await response.aread()
314
331
  hook["safely"](see, body, response.text, "event-stream" in response.headers.get("content-type", ""), counted)
315
332
  return response
@@ -38,6 +38,8 @@ def _fresh_state():
38
38
  "plantDelivered": False,
39
39
  "retrieved": [],
40
40
  "edge": [],
41
+ # The app's own model calls that failed: an attempt with one is an error, never a hold.
42
+ "modelFailures": [],
41
43
  }
42
44
 
43
45
 
@@ -442,6 +444,7 @@ class _Control(http.server.BaseHTTPRequestHandler):
442
444
  "plantDelivered": seen["plantDelivered"],
443
445
  "retrieved": seen["retrieved"],
444
446
  "edgeHeld": seen.get("edge", []),
447
+ "modelFailures": seen.get("modelFailures", []),
445
448
  }
446
449
  )
447
450
  if self.path == "/hello":
@@ -1 +0,0 @@
1
- export { t as loginCommand } from "./login-DZgP66jr.js";