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.
- package/QUICKSTART.md +3 -3
- package/README.md +9 -4
- package/SKILL.md +9 -4
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/context-throttler.d.ts +139 -0
- package/dist/lib/context-throttler.d.ts.map +1 -0
- package/dist/lib/context-throttler.js +171 -0
- package/dist/lib/context-throttler.js.map +1 -0
- package/dist/lib/coverage-gate.d.ts +155 -0
- package/dist/lib/coverage-gate.d.ts.map +1 -0
- package/dist/lib/coverage-gate.js +213 -0
- package/dist/lib/coverage-gate.js.map +1 -0
- package/dist/lib/fsm-journey.d.ts +2 -0
- package/dist/lib/fsm-journey.d.ts.map +1 -1
- package/dist/lib/fsm-journey.js +2 -0
- package/dist/lib/fsm-journey.js.map +1 -1
- package/dist/lib/goal-decomposer.d.ts +39 -0
- package/dist/lib/goal-decomposer.d.ts.map +1 -1
- package/dist/lib/goal-decomposer.js +100 -2
- package/dist/lib/goal-decomposer.js.map +1 -1
- package/dist/lib/partition-planner.d.ts +149 -0
- package/dist/lib/partition-planner.d.ts.map +1 -0
- package/dist/lib/partition-planner.js +246 -0
- package/dist/lib/partition-planner.js.map +1 -0
- package/dist/lib/route-classifier.d.ts +133 -0
- package/dist/lib/route-classifier.d.ts.map +1 -0
- package/dist/lib/route-classifier.js +217 -0
- package/dist/lib/route-classifier.js.map +1 -0
- package/package.json +3 -2
- package/scripts/check_permission.py +109 -25
- package/scripts/context_manager.py +77 -0
- package/socket.json +1 -1
|
@@ -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(
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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:
|
|
684
|
-
print(f"Expires:
|
|
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