argus-gate 1.0.0__py3-none-any.whl
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.
- argus_gate/__init__.py +97 -0
- argus_gate/action.py +196 -0
- argus_gate/adapters/__init__.py +19 -0
- argus_gate/adapters/anthropic.py +89 -0
- argus_gate/adapters/code_mode.py +123 -0
- argus_gate/adapters/google.py +89 -0
- argus_gate/adapters/model.py +142 -0
- argus_gate/adapters/openai.py +105 -0
- argus_gate/adversarial/__init__.py +38 -0
- argus_gate/adversarial/__main__.py +252 -0
- argus_gate/adversarial/client.py +133 -0
- argus_gate/adversarial/harness.py +383 -0
- argus_gate/adversarial/improve.py +173 -0
- argus_gate/constraints.py +186 -0
- argus_gate/enterprise/__init__.py +164 -0
- argus_gate/enterprise/analytics.py +290 -0
- argus_gate/enterprise/calibration.py +304 -0
- argus_gate/enterprise/character.py +199 -0
- argus_gate/enterprise/compliance.py +174 -0
- argus_gate/enterprise/dlp.py +158 -0
- argus_gate/enterprise/fleet.py +146 -0
- argus_gate/enterprise/hitl.py +195 -0
- argus_gate/enterprise/identity.py +108 -0
- argus_gate/enterprise/learning.py +225 -0
- argus_gate/enterprise/prover.py +277 -0
- argus_gate/enterprise/qcb.py +640 -0
- argus_gate/enterprise/qcb_bundle.py +271 -0
- argus_gate/enterprise/regimes.py +785 -0
- argus_gate/enterprise/registry.py +228 -0
- argus_gate/enterprise/sla.py +189 -0
- argus_gate/enterprise/sovereign.py +266 -0
- argus_gate/errors.py +70 -0
- argus_gate/evidence.py +297 -0
- argus_gate/executor.py +312 -0
- argus_gate/gtv.py +98 -0
- argus_gate/guards/__init__.py +38 -0
- argus_gate/guards/base.py +213 -0
- argus_gate/guards/database.py +137 -0
- argus_gate/guards/delegation.py +161 -0
- argus_gate/guards/file.py +161 -0
- argus_gate/guards/http.py +123 -0
- argus_gate/guards/incidents.py +281 -0
- argus_gate/guards/sandbox.py +129 -0
- argus_gate/guards/shell.py +106 -0
- argus_gate/hooks/__init__.py +20 -0
- argus_gate/hooks/a2a.py +24 -0
- argus_gate/hooks/agents_sdk.py +34 -0
- argus_gate/hooks/autogen.py +27 -0
- argus_gate/hooks/crewai.py +45 -0
- argus_gate/hooks/langgraph.py +51 -0
- argus_gate/i18n/__init__.py +158 -0
- argus_gate/i18n/locales/__init__.py +7 -0
- argus_gate/i18n/locales/ar_gulf.py +38 -0
- argus_gate/i18n/locales/ar_msa.py +39 -0
- argus_gate/i18n/locales/bn.py +38 -0
- argus_gate/i18n/locales/de.py +38 -0
- argus_gate/i18n/locales/en.py +38 -0
- argus_gate/i18n/locales/es.py +38 -0
- argus_gate/i18n/locales/fa.py +37 -0
- argus_gate/i18n/locales/fr.py +38 -0
- argus_gate/i18n/locales/hi.py +38 -0
- argus_gate/i18n/locales/id.py +37 -0
- argus_gate/i18n/locales/it.py +38 -0
- argus_gate/i18n/locales/ja.py +38 -0
- argus_gate/i18n/locales/ko.py +38 -0
- argus_gate/i18n/locales/mr.py +37 -0
- argus_gate/i18n/locales/ms.py +37 -0
- argus_gate/i18n/locales/pt.py +38 -0
- argus_gate/i18n/locales/ru.py +38 -0
- argus_gate/i18n/locales/ta.py +37 -0
- argus_gate/i18n/locales/te.py +37 -0
- argus_gate/i18n/locales/th.py +38 -0
- argus_gate/i18n/locales/tr.py +38 -0
- argus_gate/i18n/locales/ur.py +38 -0
- argus_gate/i18n/locales/vi.py +37 -0
- argus_gate/i18n/locales/zh_hans.py +38 -0
- argus_gate/i18n/locales/zh_hant.py +38 -0
- argus_gate/i18n/reason_codes.py +183 -0
- argus_gate/i18n/review.py +524 -0
- argus_gate/issuer.py +109 -0
- argus_gate/keys.py +187 -0
- argus_gate/license_service/__init__.py +18 -0
- argus_gate/license_service/service.py +350 -0
- argus_gate/mcp/__init__.py +40 -0
- argus_gate/mcp/core.py +478 -0
- argus_gate/mcp/gateway.py +454 -0
- argus_gate/mcp/http.py +408 -0
- argus_gate/mcp/jsonrpc.py +166 -0
- argus_gate/mcp/middleware.py +456 -0
- argus_gate/mcp/pinning.py +252 -0
- argus_gate/mcp/tools.py +142 -0
- argus_gate/packs/__init__.py +30 -0
- argus_gate/packs/browser_agent/mapping.md +45 -0
- argus_gate/packs/browser_agent/policy +55 -0
- argus_gate/packs/browser_agent/tool_classes.json +11 -0
- argus_gate/packs/budget.py +182 -0
- argus_gate/packs/coding_agent/mapping.md +37 -0
- argus_gate/packs/coding_agent/policy +56 -0
- argus_gate/packs/coding_agent/tool_classes.json +19 -0
- argus_gate/packs/customer_support/mapping.md +36 -0
- argus_gate/packs/customer_support/policy +53 -0
- argus_gate/packs/customer_support/tool_classes.json +12 -0
- argus_gate/packs/data_agent/mapping.md +36 -0
- argus_gate/packs/data_agent/policy +56 -0
- argus_gate/packs/data_agent/tool_classes.json +9 -0
- argus_gate/packs/loader.py +60 -0
- argus_gate/packs/robotics_agent/mapping.md +37 -0
- argus_gate/packs/robotics_agent/policy +53 -0
- argus_gate/packs/robotics_agent/tool_classes.json +8 -0
- argus_gate/quota.py +216 -0
- argus_gate/replay.py +387 -0
- argus_gate/replay_window.py +132 -0
- argus_gate/revocation.py +183 -0
- argus_gate/rust_evaluator.py +80 -0
- argus_gate/rust_wrapper.py +145 -0
- argus_gate/scope.py +296 -0
- argus_gate/stress/__init__.py +1 -0
- argus_gate/stress/analyzer.py +207 -0
- argus_gate/stress/metrics.py +270 -0
- argus_gate/stress/suite.py +630 -0
- argus_gate/stress/workload.py +161 -0
- argus_gate/thread_pool.py +58 -0
- argus_gate/token.py +378 -0
- argus_gate-1.0.0.dist-info/METADATA +100 -0
- argus_gate-1.0.0.dist-info/RECORD +126 -0
- argus_gate-1.0.0.dist-info/WHEEL +4 -0
argus_gate/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Argus Gate: capability token enforcement for AI agents.
|
|
2
|
+
|
|
3
|
+
The package implements the token protocol, the action binding, the executor
|
|
4
|
+
guard, and the issuer. Policy evaluation and evidence records come from the
|
|
5
|
+
Argus SDK and Argus Core; this package does not re-implement them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .action import (
|
|
9
|
+
GateAction,
|
|
10
|
+
action_hash,
|
|
11
|
+
canonical_action_bytes,
|
|
12
|
+
proposal_bytes,
|
|
13
|
+
proposal_hash,
|
|
14
|
+
to_proposal,
|
|
15
|
+
)
|
|
16
|
+
from .enterprise import AgentRegistry, AgentRecord, ShadowAgentError
|
|
17
|
+
from .constraints import apply_constraints, decode_constraints, encode_constraints
|
|
18
|
+
from .errors import RejectionCode, TokenProtocolError, VerificationResult
|
|
19
|
+
from .executor import ExecutorGuard, ExecutorSettings
|
|
20
|
+
from .gtv import encode as gtv_encode
|
|
21
|
+
from .issuer import DEFAULT_TTL_US, MAX_TTL_US, TokenIssuer
|
|
22
|
+
from .keys import (
|
|
23
|
+
ROLE_GATE_ISSUER,
|
|
24
|
+
ROLE_OPERATOR_AUTHORITY,
|
|
25
|
+
STATUS_ACTIVE,
|
|
26
|
+
STATUS_REVOKED,
|
|
27
|
+
STATUS_SUPERSEDED,
|
|
28
|
+
KeyEpochEntry,
|
|
29
|
+
KeyEpochList,
|
|
30
|
+
)
|
|
31
|
+
from .replay import ReplayStore, SQLiteReplayStore, open_replay_store
|
|
32
|
+
from .revocation import KIND_SEQUENCE_FROM, KIND_TOKEN_ID, RevocationEntry, RevocationSet
|
|
33
|
+
from .scope import Scope, scope_for
|
|
34
|
+
from .token import (
|
|
35
|
+
DECISION_APPROVE,
|
|
36
|
+
DECISION_MODIFY,
|
|
37
|
+
FLAG_HOLDER_BINDING,
|
|
38
|
+
TOKEN_TYPE_CAPABILITY,
|
|
39
|
+
TOKEN_TYPE_OPERATOR_OVERRIDE,
|
|
40
|
+
CapabilityToken,
|
|
41
|
+
now_us,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from importlib.metadata import version as _dist_version
|
|
46
|
+
|
|
47
|
+
__version__ = _dist_version("argus-gate")
|
|
48
|
+
except Exception:
|
|
49
|
+
__version__ = "1.0.0rc1"
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"AgentRecord",
|
|
53
|
+
"AgentRegistry",
|
|
54
|
+
"CapabilityToken",
|
|
55
|
+
"ShadowAgentError",
|
|
56
|
+
"DECISION_APPROVE",
|
|
57
|
+
"DECISION_MODIFY",
|
|
58
|
+
"DEFAULT_TTL_US",
|
|
59
|
+
"ExecutorGuard",
|
|
60
|
+
"ExecutorSettings",
|
|
61
|
+
"FLAG_HOLDER_BINDING",
|
|
62
|
+
"GateAction",
|
|
63
|
+
"KeyEpochEntry",
|
|
64
|
+
"KeyEpochList",
|
|
65
|
+
"KIND_SEQUENCE_FROM",
|
|
66
|
+
"KIND_TOKEN_ID",
|
|
67
|
+
"MAX_TTL_US",
|
|
68
|
+
"ROLE_GATE_ISSUER",
|
|
69
|
+
"ROLE_OPERATOR_AUTHORITY",
|
|
70
|
+
"RejectionCode",
|
|
71
|
+
"ReplayStore",
|
|
72
|
+
"RevocationEntry",
|
|
73
|
+
"RevocationSet",
|
|
74
|
+
"SQLiteReplayStore",
|
|
75
|
+
"STATUS_ACTIVE",
|
|
76
|
+
"STATUS_REVOKED",
|
|
77
|
+
"STATUS_SUPERSEDED",
|
|
78
|
+
"Scope",
|
|
79
|
+
"TOKEN_TYPE_CAPABILITY",
|
|
80
|
+
"TOKEN_TYPE_OPERATOR_OVERRIDE",
|
|
81
|
+
"TokenIssuer",
|
|
82
|
+
"TokenProtocolError",
|
|
83
|
+
"VerificationResult",
|
|
84
|
+
"__version__",
|
|
85
|
+
"action_hash",
|
|
86
|
+
"apply_constraints",
|
|
87
|
+
"canonical_action_bytes",
|
|
88
|
+
"decode_constraints",
|
|
89
|
+
"encode_constraints",
|
|
90
|
+
"gtv_encode",
|
|
91
|
+
"now_us",
|
|
92
|
+
"open_replay_store",
|
|
93
|
+
"proposal_bytes",
|
|
94
|
+
"proposal_hash",
|
|
95
|
+
"scope_for",
|
|
96
|
+
"to_proposal",
|
|
97
|
+
]
|
argus_gate/action.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Canonical actions and their mapping to proposals and hashes.
|
|
2
|
+
|
|
3
|
+
A Gate action is (action_class, target, arguments). Its canonical bytes are
|
|
4
|
+
the action class byte, the target with a uint16 length, and the arguments as
|
|
5
|
+
GTV. `action_hash = SHA-256(0x10 || canonical action bytes)` binds the token
|
|
6
|
+
to the exact action the executor will perform.
|
|
7
|
+
|
|
8
|
+
The action-to-proposal mapping produces a conformant proposal for policy
|
|
9
|
+
evaluation and the evidence record: the scalar leaves of the arguments value
|
|
10
|
+
become typed parameters, named by their RFC 6901 JSON Pointer with the leading
|
|
11
|
+
slash removed and slashes replaced by underscores (an alias map may override
|
|
12
|
+
the name). Leaves whose derived name is not a policy-language identifier are
|
|
13
|
+
omitted from the parameter map; they remain covered by the action hash. Nulls
|
|
14
|
+
and containers produce no parameters.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import struct
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Iterator, Mapping, Sequence
|
|
23
|
+
|
|
24
|
+
from argus.protocol.proposal import (
|
|
25
|
+
ActionClass,
|
|
26
|
+
Parameter,
|
|
27
|
+
ParameterType,
|
|
28
|
+
ProposedAction,
|
|
29
|
+
Provenance,
|
|
30
|
+
is_identifier,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .gtv import encode as gtv_encode
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"ACTION_HASH_DOMAIN",
|
|
37
|
+
"GateAction",
|
|
38
|
+
"action_hash",
|
|
39
|
+
"canonical_action_bytes",
|
|
40
|
+
"leaf_parameters",
|
|
41
|
+
"parameter_pointers",
|
|
42
|
+
"proposal_bytes",
|
|
43
|
+
"proposal_hash",
|
|
44
|
+
"to_proposal",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
ACTION_HASH_DOMAIN = 0x10
|
|
48
|
+
PROPOSAL_HASH_DOMAIN = 0x02
|
|
49
|
+
_MAX_PARAMETERS = 128
|
|
50
|
+
|
|
51
|
+
_ZERO32 = bytes(32)
|
|
52
|
+
_ZERO64 = bytes(64)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class GateAction:
|
|
57
|
+
"""One agent action in canonical form.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
action_class: An action class code (0x01 to 0x06).
|
|
61
|
+
target: The adapter-canonical target string.
|
|
62
|
+
arguments: The JSON value the executor will consume.
|
|
63
|
+
confidence: Model-reported confidence where the surface provides it.
|
|
64
|
+
timestamp: Host monotonic counter where the surface provides it.
|
|
65
|
+
context_hash: SHA-256 of the inference context where available.
|
|
66
|
+
model_identity_hash: SHA-256 of the model identifier where available.
|
|
67
|
+
model_weight_hash: SHA-256 of the model weights where available.
|
|
68
|
+
previous_attestation: Previous evidence record hash where available.
|
|
69
|
+
host_attestation: Host attestation signature where available.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
action_class: int
|
|
73
|
+
target: str
|
|
74
|
+
arguments: Any
|
|
75
|
+
confidence: float = 0.0
|
|
76
|
+
timestamp: int = 0
|
|
77
|
+
context_hash: bytes = _ZERO32
|
|
78
|
+
model_identity_hash: bytes = _ZERO32
|
|
79
|
+
model_weight_hash: bytes = _ZERO32
|
|
80
|
+
previous_attestation: bytes = _ZERO32
|
|
81
|
+
host_attestation: bytes = _ZERO64
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def canonical_action_bytes(action: GateAction) -> bytes:
|
|
85
|
+
"""The canonical action bytes (section 7.1 of the token protocol)."""
|
|
86
|
+
target_bytes = action.target.encode("utf-8")
|
|
87
|
+
if len(target_bytes) > 0xFFFF:
|
|
88
|
+
raise ValueError("target exceeds the uint16 length field")
|
|
89
|
+
return (
|
|
90
|
+
struct.pack("<B", action.action_class)
|
|
91
|
+
+ struct.pack("<H", len(target_bytes))
|
|
92
|
+
+ target_bytes
|
|
93
|
+
+ gtv_encode(action.arguments)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def action_hash(action: GateAction) -> bytes:
|
|
98
|
+
"""SHA-256(0x10 || canonical action bytes)."""
|
|
99
|
+
return hashlib.sha256(bytes([ACTION_HASH_DOMAIN]) + canonical_action_bytes(action)).digest()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pointer_escape(token: str) -> str:
|
|
103
|
+
return token.replace("~", "~0").replace("/", "~1")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _iter_leaves(value: Any, pointer: str) -> Iterator[tuple[str, Any]]:
|
|
107
|
+
if value is None:
|
|
108
|
+
return
|
|
109
|
+
if isinstance(value, dict):
|
|
110
|
+
for key in value:
|
|
111
|
+
if not isinstance(key, str):
|
|
112
|
+
raise ValueError("arguments object keys must be strings")
|
|
113
|
+
yield from _iter_leaves(value[key], pointer + "/" + _pointer_escape(key))
|
|
114
|
+
elif isinstance(value, (list, tuple)):
|
|
115
|
+
for index, element in enumerate(value):
|
|
116
|
+
yield from _iter_leaves(element, pointer + "/" + str(index))
|
|
117
|
+
else:
|
|
118
|
+
yield pointer, value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parameter_name(pointer: str, aliases: Mapping[str, str] | None) -> str | None:
|
|
122
|
+
if aliases and pointer in aliases:
|
|
123
|
+
return aliases[pointer]
|
|
124
|
+
if not pointer:
|
|
125
|
+
return None
|
|
126
|
+
name = pointer[1:].replace("/", "_")
|
|
127
|
+
return name if is_identifier(name) else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def leaf_parameters(
|
|
131
|
+
arguments: Any, aliases: Mapping[str, str] | None = None
|
|
132
|
+
) -> tuple[Parameter, ...]:
|
|
133
|
+
"""Map the scalar leaves of an arguments value to typed parameters,
|
|
134
|
+
ascending by name bytes. Leaves without an identifier-safe name are
|
|
135
|
+
omitted; they remain covered by the action hash."""
|
|
136
|
+
parameters: list[Parameter] = []
|
|
137
|
+
seen: set[str] = set()
|
|
138
|
+
for pointer, value in _iter_leaves(arguments, ""):
|
|
139
|
+
name = _parameter_name(pointer, aliases)
|
|
140
|
+
if name is None or name in seen:
|
|
141
|
+
continue
|
|
142
|
+
seen.add(name)
|
|
143
|
+
if isinstance(value, bool):
|
|
144
|
+
parameter = Parameter(name=name, type_tag=ParameterType.BOOL, value=value)
|
|
145
|
+
elif isinstance(value, (int, float)):
|
|
146
|
+
parameter = Parameter(name=name, type_tag=ParameterType.FLOAT64, value=float(value))
|
|
147
|
+
elif isinstance(value, str):
|
|
148
|
+
parameter = Parameter(name=name, type_tag=ParameterType.STRING, value=value)
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError(f"unsupported leaf type at {pointer!r}: {type(value).__name__}")
|
|
151
|
+
parameters.append(parameter)
|
|
152
|
+
if len(parameters) > _MAX_PARAMETERS:
|
|
153
|
+
raise ValueError(f"parameter map exceeds {_MAX_PARAMETERS} entries")
|
|
154
|
+
parameters.sort(key=lambda parameter: parameter.name.encode("utf-8"))
|
|
155
|
+
return tuple(parameters)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parameter_pointers(
|
|
159
|
+
arguments: Any, aliases: Mapping[str, str] | None = None
|
|
160
|
+
) -> dict[str, str]:
|
|
161
|
+
"""Map parameter names back to their JSON Pointers (the inverse of the
|
|
162
|
+
leaf mapping), for constraint translation."""
|
|
163
|
+
pointers: dict[str, str] = {}
|
|
164
|
+
for pointer, value in _iter_leaves(arguments, ""):
|
|
165
|
+
name = _parameter_name(pointer, aliases)
|
|
166
|
+
if name is not None and name not in pointers:
|
|
167
|
+
pointers[name] = pointer
|
|
168
|
+
return pointers
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def to_proposal(action: GateAction, aliases: Mapping[str, str] | None = None) -> ProposedAction:
|
|
172
|
+
"""Build the conformant proposal for policy evaluation and evidence."""
|
|
173
|
+
return ProposedAction(
|
|
174
|
+
action_class=ActionClass(action.action_class),
|
|
175
|
+
target=action.target,
|
|
176
|
+
parameters=list(leaf_parameters(action.arguments, aliases)),
|
|
177
|
+
confidence=action.confidence,
|
|
178
|
+
timestamp=action.timestamp,
|
|
179
|
+
context_hash=action.context_hash,
|
|
180
|
+
provenance=Provenance(
|
|
181
|
+
model_identity_hash=action.model_identity_hash,
|
|
182
|
+
model_weight_hash=action.model_weight_hash,
|
|
183
|
+
previous_attestation=action.previous_attestation,
|
|
184
|
+
host_attestation=action.host_attestation,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def proposal_bytes(action: GateAction, aliases: Mapping[str, str] | None = None) -> bytes:
|
|
190
|
+
"""The canonical proposal encoding (EST-SPEC-012 section 6)."""
|
|
191
|
+
return to_proposal(action, aliases).encode()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def proposal_hash(action: GateAction, aliases: Mapping[str, str] | None = None) -> bytes:
|
|
195
|
+
"""SHA-256(0x02 || canonical proposal encoding)."""
|
|
196
|
+
return to_proposal(action, aliases).proposal_hash()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Model SDK middleware: gate tool calls from OpenAI, Anthropic, and Google.
|
|
2
|
+
|
|
3
|
+
Every tool call a model emits is evaluated against the operator policy
|
|
4
|
+
before it executes; an approved call proceeds carrying a single-use
|
|
5
|
+
capability token that the executor verifies. The adapters are transport
|
|
6
|
+
neutral: they parse each provider's native shapes, gate through the shared
|
|
7
|
+
GateCore, and return the provider's own result formats.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .code_mode import CodeModeAdapter, CodeModeOutcome
|
|
11
|
+
from .model import ModelGate, ModelToolCall, ModelToolOutcome
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CodeModeAdapter",
|
|
15
|
+
"CodeModeOutcome",
|
|
16
|
+
"ModelGate",
|
|
17
|
+
"ModelToolCall",
|
|
18
|
+
"ModelToolOutcome",
|
|
19
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Anthropic tool-calling middleware: messages API tool_use shapes.
|
|
2
|
+
|
|
3
|
+
The adapter parses each tool_use block, gates it through the shared
|
|
4
|
+
ModelGate, executes approved calls, and returns tool_result blocks. A denied
|
|
5
|
+
call never executes and returns a typed error result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from .model import ModelGate, ModelGateError, ModelToolCall
|
|
14
|
+
|
|
15
|
+
__all__ = ["gated_messages_round", "parse_tool_use", "tool_result_block"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_tool_use(content_blocks: list) -> list[ModelToolCall]:
|
|
19
|
+
"""Normalize tool_use blocks from a messages API response."""
|
|
20
|
+
return [
|
|
21
|
+
ModelToolCall(
|
|
22
|
+
provider="anthropic",
|
|
23
|
+
name=block["name"],
|
|
24
|
+
arguments=block.get("input", {}),
|
|
25
|
+
call_id=block.get("id"),
|
|
26
|
+
)
|
|
27
|
+
for block in content_blocks
|
|
28
|
+
if isinstance(block, dict) and block.get("type") == "tool_use"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tool_result_block(tool_use_id: str, content: Any, is_error: bool = False) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"type": "tool_result",
|
|
35
|
+
"tool_use_id": tool_use_id,
|
|
36
|
+
"content": content if isinstance(content, str) else json.dumps(content),
|
|
37
|
+
"is_error": is_error,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def gated_messages_round(
|
|
42
|
+
create_fn: Callable,
|
|
43
|
+
*,
|
|
44
|
+
gate: ModelGate,
|
|
45
|
+
execute_tool: Callable[[str, Any], Any],
|
|
46
|
+
max_rounds: int = 6,
|
|
47
|
+
**create_kwargs,
|
|
48
|
+
) -> dict:
|
|
49
|
+
"""Drive a gated messages API loop with a client-shaped callable.
|
|
50
|
+
|
|
51
|
+
``create_fn`` accepts model, messages, tools, and any provider kwargs and
|
|
52
|
+
returns a messages-shaped dict with a content block list.
|
|
53
|
+
"""
|
|
54
|
+
transcript = list(create_kwargs["messages"])
|
|
55
|
+
decisions: list[dict] = []
|
|
56
|
+
for _ in range(max_rounds):
|
|
57
|
+
response = create_fn(**{**create_kwargs, "messages": transcript})
|
|
58
|
+
content = response.get("content") or []
|
|
59
|
+
calls = parse_tool_use(content)
|
|
60
|
+
if not calls:
|
|
61
|
+
return {"message": response, "decisions": decisions, "transcript": transcript}
|
|
62
|
+
transcript.append({"role": "assistant", "content": content})
|
|
63
|
+
result_blocks: list[dict] = []
|
|
64
|
+
for call in calls:
|
|
65
|
+
outcome = gate.evaluate(call)
|
|
66
|
+
decisions.append(
|
|
67
|
+
{
|
|
68
|
+
"provider": "anthropic",
|
|
69
|
+
"tool": call.name,
|
|
70
|
+
"decision": outcome.decision,
|
|
71
|
+
"reason": outcome.reason,
|
|
72
|
+
"clause_ref": outcome.clause_ref,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
if not outcome.approved:
|
|
76
|
+
result_blocks.append(
|
|
77
|
+
tool_result_block(call.call_id, outcome.denial_content(), is_error=True)
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
executed, value = gate.execute(outcome, execute_tool)
|
|
81
|
+
if executed:
|
|
82
|
+
result_blocks.append(
|
|
83
|
+
tool_result_block(call.call_id, json.dumps({"result": value}))
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
content = value if isinstance(value, dict) else {"error": "execution_failed"}
|
|
87
|
+
result_blocks.append(tool_result_block(call.call_id, content, is_error=True))
|
|
88
|
+
transcript.append({"role": "user", "content": result_blocks})
|
|
89
|
+
raise ModelGateError(f"exceeded max_rounds {max_rounds}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""The Code Mode adapter: the dispatch-shim for Cloudflare/Anthropic Code Mode.
|
|
2
|
+
|
|
3
|
+
Code Mode turns tools into a generated TypeScript API and runs the model's
|
|
4
|
+
script in a sandbox, with intermediate data kept out of the context. The
|
|
5
|
+
Gate's mapping: both designs keep a per-call dispatch point, so per-call
|
|
6
|
+
enforcement survives unchanged.
|
|
7
|
+
|
|
8
|
+
The adapter sits between the model's generated script and the sandbox
|
|
9
|
+
executor. The script declares its intended actions (file_read, file_write,
|
|
10
|
+
network, exec) as a sequence of GrantRequests. The adapter evaluates each
|
|
11
|
+
request against the policy, issues a capability token per granted action,
|
|
12
|
+
and the sandbox executes only the granted actions. An unbounded script is
|
|
13
|
+
refused certification; the abstention surface is INCONCLUSIVE fail-closed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Callable, Optional
|
|
20
|
+
|
|
21
|
+
from ..action import GateAction
|
|
22
|
+
from ..errors import RejectionCode, VerificationResult
|
|
23
|
+
from ..guards.base import GuardOutcome
|
|
24
|
+
from ..guards.sandbox import GRANT_CLASSES, GrantRequest, SandboxGuard, analyze_script
|
|
25
|
+
from ..mcp import GateCore
|
|
26
|
+
|
|
27
|
+
__all__ = ["CodeModeAdapter", "CodeModeOutcome"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class CodeModeOutcome:
|
|
32
|
+
"""The outcome of one code mode evaluation."""
|
|
33
|
+
|
|
34
|
+
certified: bool
|
|
35
|
+
grants: tuple[tuple[str, str], ...] # (kind, token_bytes_b64)
|
|
36
|
+
refusals: tuple[tuple[str, str], ...] # (kind, reason)
|
|
37
|
+
script_status: str # "certified" | "refused"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CodeModeAdapter:
|
|
41
|
+
"""The dispatch-shim adapter: evaluates a code mode script's intended
|
|
42
|
+
actions against the policy and issues capability tokens per grant."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
core: GateCore,
|
|
47
|
+
sandbox: SandboxGuard,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.core = core
|
|
50
|
+
self.sandbox = sandbox
|
|
51
|
+
|
|
52
|
+
def evaluate_script(
|
|
53
|
+
self,
|
|
54
|
+
source: str,
|
|
55
|
+
requests: list[GrantRequest],
|
|
56
|
+
request_id_base: int = 1,
|
|
57
|
+
) -> CodeModeOutcome:
|
|
58
|
+
"""Evaluate a code mode script: certify the script, then evaluate
|
|
59
|
+
each intended action. Returns the grants and refusals.
|
|
60
|
+
|
|
61
|
+
The script is certified if it has no unbounded constructs (via
|
|
62
|
+
analyze_script). A refused script is not evaluated further; every
|
|
63
|
+
intended action is refused with SCRIPT_NOT_CERTIFIED.
|
|
64
|
+
"""
|
|
65
|
+
analysis = analyze_script(source)
|
|
66
|
+
if not analysis.certified:
|
|
67
|
+
refusals = tuple(
|
|
68
|
+
(req.kind, "SCRIPT_NOT_CERTIFIED") for req in requests
|
|
69
|
+
)
|
|
70
|
+
return CodeModeOutcome(
|
|
71
|
+
certified=False,
|
|
72
|
+
grants=(),
|
|
73
|
+
refusals=refusals,
|
|
74
|
+
script_status="refused",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
grants: list[tuple[str, str]] = []
|
|
78
|
+
refusals: list[tuple[str, str]] = []
|
|
79
|
+
|
|
80
|
+
for i, req in enumerate(requests):
|
|
81
|
+
request_id = request_id_base + i
|
|
82
|
+
if req.kind not in GRANT_CLASSES:
|
|
83
|
+
refusals.append((req.kind, f"unknown grant kind {req.kind!r}"))
|
|
84
|
+
continue
|
|
85
|
+
outcome = self.core.evaluate_call(
|
|
86
|
+
req.kind, req.detail, request_id
|
|
87
|
+
)
|
|
88
|
+
if outcome.approved:
|
|
89
|
+
grants.append((req.kind, outcome.token.encode().hex()))
|
|
90
|
+
else:
|
|
91
|
+
refusals.append((req.kind, outcome.reason or "POLICY_DENIED"))
|
|
92
|
+
|
|
93
|
+
return CodeModeOutcome(
|
|
94
|
+
certified=len(refusals) == 0,
|
|
95
|
+
grants=tuple(grants),
|
|
96
|
+
refusals=tuple(refusals),
|
|
97
|
+
script_status="certified" if len(refusals) == 0 else "refused",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def execute_grants(
|
|
101
|
+
self,
|
|
102
|
+
grants: tuple[tuple[str, str], ...],
|
|
103
|
+
run_fns: dict[str, Callable[[Any], Any]],
|
|
104
|
+
) -> dict[str, GuardOutcome]:
|
|
105
|
+
"""Execute each granted action with its capability token.
|
|
106
|
+
|
|
107
|
+
Each grant carries the hex-encoded token bytes and the action kind.
|
|
108
|
+
run_fns maps kind -> the function to execute with the granted detail.
|
|
109
|
+
Returns a dict of kind -> GuardOutcome for each executed grant.
|
|
110
|
+
"""
|
|
111
|
+
results: dict[str, GuardOutcome] = {}
|
|
112
|
+
for kind, token_hex in grants:
|
|
113
|
+
token_bytes = bytes.fromhex(token_hex)
|
|
114
|
+
if kind not in run_fns:
|
|
115
|
+
continue
|
|
116
|
+
# Reconstruct a GrantRequest for the sandbox guard
|
|
117
|
+
# The detail is not carried in the grant; the caller must
|
|
118
|
+
# reconstruct it from the original script context.
|
|
119
|
+
# This is a design boundary: the adapter issues the grant,
|
|
120
|
+
# the caller executes it with the detail from the script.
|
|
121
|
+
request = GrantRequest(kind=kind, detail={}, request_id=None)
|
|
122
|
+
results[kind] = self.sandbox.grant(token_bytes, request, run_fns[kind])
|
|
123
|
+
return results
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Google tool-calling middleware: generateContent functionCall shapes.
|
|
2
|
+
|
|
3
|
+
The adapter parses each functionCall part, gates it through the shared
|
|
4
|
+
ModelGate, and returns functionResponse parts. Protocol-conformant with the
|
|
5
|
+
OpenAI and Anthropic adapters: the same policy produces the same decisions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from .model import ModelGate, ModelGateError, ModelToolCall
|
|
14
|
+
|
|
15
|
+
__all__ = ["function_response_part", "gated_generate_round", "parse_function_calls"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_function_calls(response: dict) -> list[ModelToolCall]:
|
|
19
|
+
"""Normalize functionCall parts from a generateContent response."""
|
|
20
|
+
calls: list[ModelToolCall] = []
|
|
21
|
+
for candidate in response.get("candidates") or []:
|
|
22
|
+
content = candidate.get("content") or {}
|
|
23
|
+
for part in content.get("parts") or []:
|
|
24
|
+
function_call = part.get("functionCall")
|
|
25
|
+
if isinstance(function_call, dict) and function_call.get("name"):
|
|
26
|
+
calls.append(
|
|
27
|
+
ModelToolCall(
|
|
28
|
+
provider="google",
|
|
29
|
+
name=function_call["name"],
|
|
30
|
+
arguments=function_call.get("args", {}),
|
|
31
|
+
call_id=function_call.get("id"),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return calls
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def function_response_part(name: str, result: Any) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"functionResponse": {
|
|
40
|
+
"name": name,
|
|
41
|
+
"response": result if isinstance(result, dict) else {"result": result},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def gated_generate_round(
|
|
47
|
+
create_fn: Callable,
|
|
48
|
+
*,
|
|
49
|
+
gate: ModelGate,
|
|
50
|
+
execute_tool: Callable[[str, Any], Any],
|
|
51
|
+
max_rounds: int = 6,
|
|
52
|
+
**create_kwargs,
|
|
53
|
+
) -> dict:
|
|
54
|
+
"""Drive a gated generateContent loop with a client-shaped callable."""
|
|
55
|
+
contents = list(create_kwargs["contents"])
|
|
56
|
+
decisions: list[dict] = []
|
|
57
|
+
for _ in range(max_rounds):
|
|
58
|
+
response = create_fn(**{**create_kwargs, "contents": contents})
|
|
59
|
+
calls = parse_function_calls(response)
|
|
60
|
+
candidates = response.get("candidates") or []
|
|
61
|
+
model_content = (candidates[0].get("content") if candidates else {}) or {}
|
|
62
|
+
if not calls:
|
|
63
|
+
return {"message": response, "decisions": decisions, "contents": contents}
|
|
64
|
+
contents.append(model_content)
|
|
65
|
+
response_parts: list[dict] = []
|
|
66
|
+
for call in calls:
|
|
67
|
+
outcome = gate.evaluate(call)
|
|
68
|
+
decisions.append(
|
|
69
|
+
{
|
|
70
|
+
"provider": "google",
|
|
71
|
+
"tool": call.name,
|
|
72
|
+
"decision": outcome.decision,
|
|
73
|
+
"reason": outcome.reason,
|
|
74
|
+
"clause_ref": outcome.clause_ref,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
if not outcome.approved:
|
|
78
|
+
response_parts.append(
|
|
79
|
+
function_response_part(call.name, json.loads(outcome.denial_content()))
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
executed, value = gate.execute(outcome, execute_tool)
|
|
83
|
+
if executed:
|
|
84
|
+
response_parts.append(function_response_part(call.name, {"result": value}))
|
|
85
|
+
else:
|
|
86
|
+
content = value if isinstance(value, dict) else {"error": "execution_failed"}
|
|
87
|
+
response_parts.append(function_response_part(call.name, content))
|
|
88
|
+
contents.append({"role": "user", "parts": response_parts})
|
|
89
|
+
raise ModelGateError(f"exceeded max_rounds {max_rounds}")
|