network-ai 5.2.2 → 5.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.
@@ -41,6 +41,17 @@ GRANT_TOKEN_TTL_MINUTES = 5
41
41
  GRANTS_FILE = Path(__file__).parent.parent / "data" / "active_grants.json"
42
42
  AUDIT_LOG = Path(__file__).parent.parent / "data" / "audit_log.jsonl"
43
43
 
44
+ # ADVISORY TOKENS — IMPORTANT
45
+ # Grant tokens produced by this script are ADVISORY scoring outputs only.
46
+ # They are NOT authenticated credentials. The agent_id supplied via --agent
47
+ # is accepted as-is from the caller and is NOT verified against an identity
48
+ # provider. Downstream systems MUST treat these tokens as hints, not proof
49
+ # of identity, and SHOULD require a separate authenticated session or human
50
+ # approval before honouring access to sensitive resources.
51
+ #
52
+ # For PAYMENTS, DATABASE (write), and FILE_EXPORT the caller must also pass
53
+ # --confirm-high-risk to acknowledge the advisory-only nature of the grant.
54
+
44
55
  # Default trust levels for known agents
45
56
  DEFAULT_TRUST_LEVELS = {
46
57
  "orchestrator": 0.9,
@@ -49,6 +60,13 @@ DEFAULT_TRUST_LEVELS = {
49
60
  "risk_assessor": 0.85,
50
61
  }
51
62
 
63
+ # Agents whose identity is pre-registered. Any agent_id NOT in this set
64
+ # receives a reduced trust score (0.3) and triggers an advisory warning.
65
+ KNOWN_AGENTS: set[str] = set(DEFAULT_TRUST_LEVELS.keys())
66
+
67
+ # Resource types that require --confirm-high-risk before a grant is issued
68
+ HIGH_RISK_RESOURCES: set[str] = {"PAYMENTS", "DATABASE"}
69
+
52
70
  # Base risk scores for resource types
53
71
  BASE_RISKS = {
54
72
  "DATABASE": 0.5, # Internal database access
@@ -258,48 +276,88 @@ def save_grant(grant: dict[str, Any]) -> None:
258
276
  GRANTS_FILE.write_text(json.dumps(grants, indent=2))
259
277
 
260
278
 
261
- def evaluate_permission(agent_id: str, resource_type: str,
262
- justification: str, scope: Optional[str] = None) -> dict[str, Any]:
279
+ def evaluate_permission(
280
+ agent_id: str,
281
+ resource_type: str,
282
+ justification: str,
283
+ scope: Optional[str] = None,
284
+ confirm_high_risk: bool = False,
285
+ ) -> dict[str, Any]:
263
286
  """
264
287
  Evaluate a permission request using weighted scoring.
265
-
288
+
289
+ NOTE: These tokens are ADVISORY ONLY — the caller supplies their own
290
+ agent_id and it is not authenticated. Downstream systems must not treat
291
+ the resulting token as proof of identity without additional verification.
292
+
266
293
  Weights:
267
294
  - Justification Quality: 40%
268
295
  - Agent Trust Level: 30%
269
296
  - Risk Assessment: 30%
270
297
  """
298
+ # Warn if agent_id is not in the pre-registered known-agents list
299
+ unknown_agent = agent_id not in KNOWN_AGENTS
300
+
271
301
  # Log the request
272
302
  log_audit("permission_request", {
273
303
  "agent_id": agent_id,
274
304
  "resource_type": resource_type,
275
305
  "justification": justification,
276
- "scope": scope
306
+ "scope": scope,
307
+ "unknown_agent": unknown_agent,
277
308
  })
278
-
309
+
310
+ # Require explicit acknowledgement for high-risk resources
311
+ needs_confirmation = (
312
+ resource_type in HIGH_RISK_RESOURCES
313
+ or (resource_type == "DATABASE" and scope and re.search(
314
+ r'\b(write|delete|update|modify|create)\b', scope, re.IGNORECASE
315
+ ))
316
+ )
317
+ if needs_confirmation and not confirm_high_risk:
318
+ return {
319
+ "granted": False,
320
+ "advisory": True,
321
+ "reason": (
322
+ f"{resource_type} is a high-risk resource. Re-run with --confirm-high-risk "
323
+ "to acknowledge that this token is advisory only and does not authenticate "
324
+ "the supplied agent identity."
325
+ ),
326
+ "scores": {"justification": None, "trust": None, "risk": None},
327
+ }
328
+
279
329
  # 1. Justification Quality (40% weight)
280
330
  justification_score = score_justification(justification)
281
331
  if justification_score < 0.3:
282
332
  return {
283
333
  "granted": False,
334
+ "advisory": True,
284
335
  "reason": "Justification is insufficient. Please provide specific task context.",
285
336
  "scores": {
286
337
  "justification": justification_score,
287
338
  "trust": None,
288
- "risk": None
289
- }
339
+ "risk": None,
340
+ },
290
341
  }
291
-
342
+
292
343
  # 2. Agent Trust Level (30% weight)
293
- trust_level = DEFAULT_TRUST_LEVELS.get(agent_id, 0.5)
344
+ # Unknown agents receive a reduced base trust score (0.3) — their identity
345
+ # has not been pre-registered and cannot be verified by this script.
346
+ trust_level = DEFAULT_TRUST_LEVELS.get(agent_id, 0.3) if not unknown_agent \
347
+ else 0.3
294
348
  if trust_level < 0.4:
295
349
  return {
296
350
  "granted": False,
297
- "reason": "Agent trust level is below threshold. Escalate to human operator.",
351
+ "advisory": True,
352
+ "reason": (
353
+ "Agent trust level is below threshold. Escalate to human operator."
354
+ + (" (unrecognized agent_id — identity not pre-registered)" if unknown_agent else "")
355
+ ),
298
356
  "scores": {
299
357
  "justification": justification_score,
300
358
  "trust": trust_level,
301
- "risk": None
302
- }
359
+ "risk": None,
360
+ },
303
361
  }
304
362
 
305
363
  # 3. Risk Assessment (30% weight)
@@ -307,12 +365,13 @@ def evaluate_permission(agent_id: str, resource_type: str,
307
365
  if risk_score > 0.8:
308
366
  return {
309
367
  "granted": False,
368
+ "advisory": True,
310
369
  "reason": "Risk assessment exceeds acceptable threshold. Narrow the requested scope.",
311
370
  "scores": {
312
371
  "justification": justification_score,
313
372
  "trust": trust_level,
314
- "risk": risk_score
315
- }
373
+ "risk": risk_score,
374
+ },
316
375
  }
317
376
 
318
377
  # Calculate weighted approval score
@@ -325,13 +384,14 @@ def evaluate_permission(agent_id: str, resource_type: str,
325
384
  if weighted_score < 0.5:
326
385
  return {
327
386
  "granted": False,
387
+ "advisory": True,
328
388
  "reason": f"Combined evaluation score ({weighted_score:.2f}) below threshold (0.5).",
329
389
  "scores": {
330
390
  "justification": justification_score,
331
391
  "trust": trust_level,
332
392
  "risk": risk_score,
333
- "weighted": weighted_score
334
- }
393
+ "weighted": weighted_score,
394
+ },
335
395
  }
336
396
 
337
397
  # Generate grant
@@ -346,15 +406,19 @@ def evaluate_permission(agent_id: str, resource_type: str,
346
406
  "scope": scope,
347
407
  "expires_at": expires_at,
348
408
  "restrictions": restrictions,
349
- "granted_at": datetime.now(timezone.utc).isoformat()
409
+ "granted_at": datetime.now(timezone.utc).isoformat(),
410
+ "advisory": True, # Always advisory — not an authenticated credential
411
+ "unknown_agent": unknown_agent,
350
412
  }
351
-
413
+
352
414
  # Save grant and log
353
415
  save_grant(grant)
354
416
  log_audit("permission_granted", grant)
355
-
417
+
356
418
  return {
357
419
  "granted": True,
420
+ "advisory": True, # Token is advisory — agent identity was not verified
421
+ "unknown_agent": unknown_agent,
358
422
  "token": token,
359
423
  "expires_at": expires_at,
360
424
  "restrictions": restrictions,
@@ -362,8 +426,14 @@ def evaluate_permission(agent_id: str, resource_type: str,
362
426
  "justification": justification_score,
363
427
  "trust": trust_level,
364
428
  "risk": risk_score,
365
- "weighted": weighted_score
366
- }
429
+ "weighted": weighted_score,
430
+ },
431
+ "notice": (
432
+ "This token was issued based on local scoring only. "
433
+ "The agent identity supplied via --agent was NOT cryptographically verified. "
434
+ "Treat this token as advisory and require human approval before granting "
435
+ "access to sensitive systems."
436
+ ),
367
437
  }
368
438
 
369
439
 
@@ -649,6 +719,16 @@ Examples:
649
719
  action="store_true",
650
720
  help="Output result as JSON"
651
721
  )
722
+ parser.add_argument(
723
+ "--confirm-high-risk",
724
+ action="store_true",
725
+ dest="confirm_high_risk",
726
+ help=(
727
+ "Required for PAYMENTS and DATABASE resources. Acknowledges that the issued "
728
+ "token is advisory only and that the caller-supplied agent identity was not "
729
+ "cryptographically verified."
730
+ ),
731
+ )
652
732
 
653
733
  args = parser.parse_args()
654
734
 
@@ -672,17 +752,21 @@ Examples:
672
752
  agent_id=args.agent,
673
753
  resource_type=args.resource,
674
754
  justification=args.justification,
675
- scope=args.scope
755
+ scope=args.scope,
756
+ confirm_high_risk=getattr(args, "confirm_high_risk", False),
676
757
  )
677
758
 
678
759
  if args.json:
679
760
  print(json.dumps(result, indent=2))
680
761
  else:
681
762
  if result["granted"]:
682
- print("GRANTED")
683
- print(f"Token: {result['token']}")
684
- print(f"Expires: {result['expires_at']}")
763
+ print("GRANTED [ADVISORY — agent identity was NOT verified]")
764
+ print(f"Token: {result['token']}")
765
+ print(f"Expires: {result['expires_at']}")
685
766
  print(f"Restrictions: {', '.join(result['restrictions'])}")
767
+ if result.get("unknown_agent"):
768
+ print("WARNING: agent_id is not in the pre-registered known-agents list.")
769
+ print(f"\nNOTICE: {result.get('notice', '')}")
686
770
  else:
687
771
  print("DENIED")
688
772
  print(f"Reason: {result['reason']}")
@@ -74,6 +74,72 @@ def _now_iso() -> str:
74
74
  return datetime.now(timezone.utc).isoformat()
75
75
 
76
76
 
77
+ def _validate_context(ctx: dict[str, Any]) -> list[str]:
78
+ """
79
+ Validate the project context file against the expected schema.
80
+
81
+ Returns a list of warning strings (empty = clean).
82
+ Checks:
83
+ - Required top-level keys are present
84
+ - String fields are not excessively long (injection/poisoning guard)
85
+ - List entries are strings or dicts, not executable-looking content
86
+ - No obvious prompt-injection patterns in goals, decisions, or banned entries
87
+ """
88
+ import re as _re
89
+ warnings: list[str] = []
90
+
91
+ REQUIRED_KEYS = {"project", "goals", "stack", "milestones", "decisions",
92
+ "banned_approaches", "updated_at"}
93
+ missing = REQUIRED_KEYS - set(ctx.keys())
94
+ if missing:
95
+ warnings.append(f"Missing keys in context file: {', '.join(sorted(missing))}")
96
+
97
+ # Field length caps
98
+ project = ctx.get("project", {})
99
+ for field in ("name", "description", "version"):
100
+ val = project.get(field, "")
101
+ if isinstance(val, str) and len(val) > 500:
102
+ warnings.append(f"project.{field} exceeds 500 characters \u2014 consider shortening.")
103
+
104
+ # Injection pattern check on free-text list fields
105
+ INJECTION_RE = _re.compile(
106
+ r'ignore\s+(previous|above|prior|all)|override\s+(policy|restriction|rule)|'
107
+ r'system\s*prompt|you\s+are\s+(now|a)|act\s+as\s+(if|a|an)|'
108
+ r'pretend\s+(to|that|you)|bypass\s+(security|check|restriction)|'
109
+ r'disregard\s+(policy|rule)|admin\s+(mode|access|override)|'
110
+ r'\bsudo\b|\bjailbreak\b',
111
+ _re.IGNORECASE,
112
+ )
113
+
114
+ def _check_text(label: str, text: str) -> None:
115
+ if INJECTION_RE.search(text):
116
+ warnings.append(
117
+ f"Possible injection pattern detected in {label}: {text[:80]!r}"
118
+ )
119
+ if len(text) > 2000:
120
+ warnings.append(f"{label} entry exceeds 2000 characters \u2014 review before injecting.")
121
+
122
+ for i, goal in enumerate(ctx.get("goals", [])):
123
+ if isinstance(goal, str):
124
+ _check_text(f"goals[{i}]", goal)
125
+
126
+ for i, dec in enumerate(ctx.get("decisions", [])):
127
+ if isinstance(dec, dict):
128
+ dec_dict = cast(dict[str, object], dec)
129
+ for fld in ("decision", "rationale"):
130
+ fld_val = dec_dict.get(fld)
131
+ if isinstance(fld_val, str):
132
+ _check_text(f"decisions[{i}].{fld}", fld_val)
133
+ elif isinstance(dec, str):
134
+ _check_text(f"decisions[{i}]", dec)
135
+
136
+ for i, banned in enumerate(ctx.get("banned_approaches", [])):
137
+ if isinstance(banned, str):
138
+ _check_text(f"banned_approaches[{i}]", banned)
139
+
140
+ return warnings
141
+
142
+
77
143
  def _load() -> dict[str, Any]:
78
144
  if not CONTEXT_PATH.exists():
79
145
  print(
@@ -127,6 +193,11 @@ def cmd_init(args: argparse.Namespace) -> int:
127
193
 
128
194
  def cmd_show(args: argparse.Namespace) -> int: # noqa: ARG001
129
195
  ctx = _load()
196
+ warnings = _validate_context(ctx)
197
+ if warnings:
198
+ print("[context_manager] VALIDATION WARNINGS — review before injecting:", file=sys.stderr)
199
+ for w in warnings:
200
+ print(f" ! {w}", file=sys.stderr)
130
201
  print(json.dumps(ctx, indent=2))
131
202
  return 0
132
203
 
@@ -134,6 +205,12 @@ def cmd_show(args: argparse.Namespace) -> int: # noqa: ARG001
134
205
  def cmd_inject(args: argparse.Namespace) -> int: # noqa: ARG001
135
206
  """Print a formatted block suitable for injection into an agent system prompt."""
136
207
  ctx = _load()
208
+ warnings = _validate_context(ctx)
209
+ if warnings:
210
+ print("[context_manager] VALIDATION WARNINGS \u2014 context has potential issues:", file=sys.stderr)
211
+ for w in warnings:
212
+ print(f" ! {w}", file=sys.stderr)
213
+ print("[context_manager] Proceeding with inject, but review warnings above.", file=sys.stderr)
137
214
  p = ctx.get("project", {})
138
215
 
139
216
  lines: list[str] = []
package/socket.json CHANGED
@@ -1,4 +1,4 @@
1
- {
1
+ {
2
2
  "version": 2,
3
3
  "ignore": {
4
4
  "evalDynamicCodeExecution": [