@intutic/sync-daemon 2.2.0 → 2.3.0
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/README.md +1 -1
- package/dist/agentReporter.d.ts +23 -11
- package/dist/agentReporter.d.ts.map +1 -1
- package/dist/agentReporter.js +20 -13
- package/dist/agentReporter.js.map +1 -1
- package/dist/configReader.d.ts +1 -3
- package/dist/configReader.d.ts.map +1 -1
- package/dist/configReader.js +0 -2
- package/dist/configReader.js.map +1 -1
- package/dist/configWriter.d.ts +11 -12
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +35 -58
- package/dist/configWriter.js.map +1 -1
- package/dist/disconnect/harnesses.d.ts +0 -2
- package/dist/disconnect/harnesses.d.ts.map +1 -1
- package/dist/disconnect/harnesses.js +157 -80
- package/dist/disconnect/harnesses.js.map +1 -1
- package/dist/disconnect/index.d.ts.map +1 -1
- package/dist/disconnect/index.js +11 -1
- package/dist/disconnect/index.js.map +1 -1
- package/dist/disconnect/mcp.d.ts +21 -0
- package/dist/disconnect/mcp.d.ts.map +1 -1
- package/dist/disconnect/mcp.js +71 -0
- package/dist/disconnect/mcp.js.map +1 -1
- package/dist/disconnect/recognise.d.ts +2 -2
- package/dist/disconnect/recognise.d.ts.map +1 -1
- package/dist/disconnect/recognise.js +15 -5
- package/dist/disconnect/recognise.js.map +1 -1
- package/dist/harness/aiderConfigMerger.d.ts +28 -8
- package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
- package/dist/harness/aiderConfigMerger.js +59 -11
- package/dist/harness/aiderConfigMerger.js.map +1 -1
- package/dist/harness/antigravityCliHooks.d.ts +61 -0
- package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
- package/dist/harness/antigravityCliHooks.js +207 -0
- package/dist/harness/antigravityCliHooks.js.map +1 -0
- package/dist/harness/antigravityHooks.d.ts +12 -2
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +36 -16
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/antigravityProducts.d.ts +40 -0
- package/dist/harness/antigravityProducts.d.ts.map +1 -0
- package/dist/harness/antigravityProducts.js +109 -0
- package/dist/harness/antigravityProducts.js.map +1 -0
- package/dist/harness/claudeAgentsMd.d.ts +29 -0
- package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
- package/dist/harness/claudeAgentsMd.js +80 -0
- package/dist/harness/claudeAgentsMd.js.map +1 -0
- package/dist/harness/claudeCodeHooks.d.ts +6 -5
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +98 -24
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/clineHooks.d.ts +0 -1
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +10 -4
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexHooks.d.ts +2 -0
- package/dist/harness/codexHooks.d.ts.map +1 -1
- package/dist/harness/codexHooks.js +12 -4
- package/dist/harness/codexHooks.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +5 -2
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +9 -3
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +5 -5
- package/dist/harness/dshHooks.d.ts.map +1 -1
- package/dist/harness/dshHooks.js +5 -5
- package/dist/harness/gateArtifacts.d.ts +35 -0
- package/dist/harness/gateArtifacts.d.ts.map +1 -0
- package/dist/harness/gateArtifacts.js +88 -0
- package/dist/harness/gateArtifacts.js.map +1 -0
- package/dist/harness/gateBody.d.ts +186 -69
- package/dist/harness/gateBody.d.ts.map +1 -1
- package/dist/harness/gateBody.js +820 -182
- package/dist/harness/gateBody.js.map +1 -1
- package/dist/harness/gateSightings.d.ts +47 -0
- package/dist/harness/gateSightings.d.ts.map +1 -0
- package/dist/harness/gateSightings.js +159 -0
- package/dist/harness/gateSightings.js.map +1 -0
- package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
- package/dist/harness/githubCopilotHooks.js +7 -1
- package/dist/harness/githubCopilotHooks.js.map +1 -1
- package/dist/harness/gooseHardener.d.ts +0 -1
- package/dist/harness/gooseHardener.d.ts.map +1 -1
- package/dist/harness/gooseHardener.js +1 -1
- package/dist/harness/gooseHardener.js.map +1 -1
- package/dist/harness/gooseHooks.d.ts +14 -1
- package/dist/harness/gooseHooks.d.ts.map +1 -1
- package/dist/harness/gooseHooks.js +55 -29
- package/dist/harness/gooseHooks.js.map +1 -1
- package/dist/harness/grokHooks.d.ts +2 -2
- package/dist/harness/grokHooks.d.ts.map +1 -1
- package/dist/harness/grokHooks.js +11 -6
- package/dist/harness/grokHooks.js.map +1 -1
- package/dist/harness/hermesHooks.d.ts +18 -8
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +48 -17
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/mcpAutoWrite.d.ts +24 -9
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +218 -25
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/museHooks.d.ts +3 -3
- package/dist/harness/museHooks.d.ts.map +1 -1
- package/dist/harness/museHooks.js +9 -5
- package/dist/harness/museHooks.js.map +1 -1
- package/dist/harness/n8nHooks.d.ts +0 -1
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +4 -1
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +2 -2
- package/dist/harness/openCodeHooks.d.ts.map +1 -1
- package/dist/harness/openCodeHooks.js +7 -3
- package/dist/harness/openCodeHooks.js.map +1 -1
- package/dist/harness/openWebuiHooks.d.ts +0 -1
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +5 -1
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openclawHooks.d.ts +70 -18
- package/dist/harness/openclawHooks.d.ts.map +1 -1
- package/dist/harness/openclawHooks.js +213 -161
- package/dist/harness/openclawHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts +24 -10
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +49 -48
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts +52 -17
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +177 -166
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +107 -25
- package/dist/harness/protectedPaths.d.ts.map +1 -1
- package/dist/harness/protectedPaths.js +342 -48
- package/dist/harness/protectedPaths.js.map +1 -1
- package/dist/harness/rulesSection.d.ts +62 -0
- package/dist/harness/rulesSection.d.ts.map +1 -0
- package/dist/harness/rulesSection.js +145 -0
- package/dist/harness/rulesSection.js.map +1 -0
- package/dist/harness/windsurfHooks.d.ts +0 -1
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +4 -1
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +53 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +133 -0
- package/dist/inventory.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts +2 -1
- package/dist/lib/complianceProbes.d.ts.map +1 -1
- package/dist/lib/complianceProbes.js +1 -1
- package/dist/lib/complianceProbes.js.map +1 -1
- package/dist/lib/decisionsDigest.d.ts +34 -16
- package/dist/lib/decisionsDigest.d.ts.map +1 -1
- package/dist/lib/decisionsDigest.js +105 -46
- package/dist/lib/decisionsDigest.js.map +1 -1
- package/dist/lib/egressPolicy.d.ts +2 -2
- package/dist/lib/egressPolicy.js +2 -2
- package/dist/lib/mcpRegistryPy.d.ts +11 -0
- package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
- package/dist/lib/mcpRegistryPy.js +173 -0
- package/dist/lib/mcpRegistryPy.js.map +1 -0
- package/dist/lib/phrasesPy.d.ts +11 -0
- package/dist/lib/phrasesPy.d.ts.map +1 -0
- package/dist/lib/phrasesPy.js +351 -0
- package/dist/lib/phrasesPy.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +46 -10
- package/dist/lib/policySnapshot.d.ts.map +1 -1
- package/dist/lib/policySnapshot.js +136 -47
- package/dist/lib/policySnapshot.js.map +1 -1
- package/dist/lib/processPoller.d.ts.map +1 -1
- package/dist/lib/processPoller.js +9 -2
- package/dist/lib/processPoller.js.map +1 -1
- package/dist/lib/regexLinearity.d.ts +43 -0
- package/dist/lib/regexLinearity.d.ts.map +1 -0
- package/dist/lib/regexLinearity.js +275 -0
- package/dist/lib/regexLinearity.js.map +1 -0
- package/dist/lib/runtimeEnv.d.ts +1 -3
- package/dist/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/lib/runtimeEnv.js +0 -2
- package/dist/lib/runtimeEnv.js.map +1 -1
- package/dist/lib/sequencePy.d.ts +10 -0
- package/dist/lib/sequencePy.d.ts.map +1 -0
- package/dist/lib/sequencePy.js +102 -0
- package/dist/lib/sequencePy.js.map +1 -0
- package/dist/sessionReporter.d.ts +17 -11
- package/dist/sessionReporter.d.ts.map +1 -1
- package/dist/sessionReporter.js +36 -23
- package/dist/sessionReporter.js.map +1 -1
- package/dist/skillWriter.d.ts +23 -5
- package/dist/skillWriter.d.ts.map +1 -1
- package/dist/skillWriter.js +159 -16
- package/dist/skillWriter.js.map +1 -1
- package/dist/syncCycle.d.ts.map +1 -1
- package/dist/syncCycle.js +21 -9
- package/dist/syncCycle.js.map +1 -1
- package/dist/trajectoryMonitor.d.ts +1 -3
- package/dist/trajectoryMonitor.d.ts.map +1 -1
- package/dist/trajectoryMonitor.js +1 -4
- package/dist/trajectoryMonitor.js.map +1 -1
- package/dist/watcher/driftWatcher.d.ts +0 -1
- package/dist/watcher/driftWatcher.d.ts.map +1 -1
- package/dist/watcher/driftWatcher.js +1 -2
- package/dist/watcher/driftWatcher.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts +33 -7
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +373 -61
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/dist/watcher/vscodeHookSettings.d.ts +30 -0
- package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
- package/dist/watcher/vscodeHookSettings.js +170 -0
- package/dist/watcher/vscodeHookSettings.js.map +1 -0
- package/dist/wsClient.d.ts +0 -1
- package/dist/wsClient.d.ts.map +1 -1
- package/dist/wsClient.js +0 -1
- package/dist/wsClient.js.map +1 -1
- package/package.json +5 -5
- package/dist/harness/claudeDesktopHooks.d.ts +0 -33
- package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
- package/dist/harness/claudeDesktopHooks.js +0 -256
- package/dist/harness/claudeDesktopHooks.js.map +0 -1
- package/dist/harness/continueHooks.d.ts +0 -63
- package/dist/harness/continueHooks.d.ts.map +0 -1
- package/dist/harness/continueHooks.js +0 -250
- package/dist/harness/continueHooks.js.map +0 -1
- package/dist/harness/gateKind.d.ts +0 -164
- package/dist/harness/gateKind.d.ts.map +0 -1
- package/dist/harness/gateKind.js +0 -144
- package/dist/harness/gateKind.js.map +0 -1
- package/dist/harness/rooCodeHooks.d.ts +0 -31
- package/dist/harness/rooCodeHooks.d.ts.map +0 -1
- package/dist/harness/rooCodeHooks.js +0 -281
- package/dist/harness/rooCodeHooks.js.map +0 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server registry and allowlist decisions in Python, for the bash
|
|
3
|
+
* gates (their extractor already requires python3).
|
|
4
|
+
*
|
|
5
|
+
* Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/mcp_registry.py`,
|
|
6
|
+
* which a test compares; edit that file and copy it here. It is the
|
|
7
|
+
* transliteration of `@intutic/shared-types`'s mcpRegistryRecord.ts, and both
|
|
8
|
+
* run the shared vectors.
|
|
9
|
+
*/
|
|
10
|
+
export const MCP_REGISTRY_PY_SOURCE = String.raw `"""The MCP server registry and allowlist decisions, read from the policy snapshot.
|
|
11
|
+
|
|
12
|
+
A transliteration of packages/shared-types/src/mcpRegistryRecord.ts: the
|
|
13
|
+
registry decision (evaluateMcpRegistry), the allowlist decision
|
|
14
|
+
(evaluateMcpAllowlist), and the snapshot's @mcp_registry and @mcp_allowlist
|
|
15
|
+
records. The control plane's hook gate, the MCP proxy and the JavaScript hook
|
|
16
|
+
gates run the TypeScript; the bash hook gates and this package run this file,
|
|
17
|
+
and every one of them runs packages/shared-types/fixtures/mcp-registry-vectors.json,
|
|
18
|
+
so all of them refuse a call with the same code, rule id and reason.
|
|
19
|
+
|
|
20
|
+
The bash gates emit this file verbatim (services/sync-daemon/src/lib/mcpRegistryPy.ts
|
|
21
|
+
holds a byte-identical copy, which a test compares). It must stay
|
|
22
|
+
self-contained: the standard library only, no module state beyond the
|
|
23
|
+
constants below, and no backtick or dollar-brace, which the TypeScript copy
|
|
24
|
+
cannot hold.
|
|
25
|
+
|
|
26
|
+
Both records sit inside the snapshot's digest. A gate that finds the snapshot
|
|
27
|
+
unverified (its digest broken or missing, or another workspace's) trusts
|
|
28
|
+
neither: it refuses every MCP call with unverified_refusal, at block, in an
|
|
29
|
+
observe-only workspace too.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import base64
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
MCP_REGISTRY_RECORD_TAG = "@mcp_registry"
|
|
36
|
+
MCP_ALLOWLIST_RECORD_TAG = "@mcp_allowlist"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def split_mcp_tool_name(tool):
|
|
40
|
+
"""(server, tool) for a harness name mcp__<server>__<tool>, else None.
|
|
41
|
+
|
|
42
|
+
The server is the part before the first double underscore; the tool, the
|
|
43
|
+
rest, may hold more of them.
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(tool, str) or not tool.startswith("mcp__"):
|
|
46
|
+
return None
|
|
47
|
+
rest = tool[len("mcp__"):]
|
|
48
|
+
sep = rest.find("__")
|
|
49
|
+
if sep <= 0:
|
|
50
|
+
return None
|
|
51
|
+
return rest[:sep], rest[sep + 2:]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _strings(value):
|
|
55
|
+
return [v for v in value if isinstance(v, str)] if isinstance(value, list) else []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_registry(value):
|
|
59
|
+
"""The registry as the control plane sends it, or None for a non-object.
|
|
60
|
+
|
|
61
|
+
Wrong-typed lists read as empty, and any default but deny as allow.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(value, dict):
|
|
64
|
+
return None
|
|
65
|
+
disabled = {}
|
|
66
|
+
if isinstance(value.get("disabledTools"), dict):
|
|
67
|
+
for server, tools in value["disabledTools"].items():
|
|
68
|
+
names = _strings(tools)
|
|
69
|
+
if names:
|
|
70
|
+
disabled[server] = names
|
|
71
|
+
return {
|
|
72
|
+
"defaultPolicy": "deny" if value.get("defaultPolicy") == "deny" else "allow",
|
|
73
|
+
"approvedServers": _strings(value.get("approvedServers")),
|
|
74
|
+
"blockedServers": _strings(value.get("blockedServers")),
|
|
75
|
+
"heldServers": _strings(value.get("heldServers")),
|
|
76
|
+
"disabledTools": disabled,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def decode_registry_record(line):
|
|
81
|
+
"""The @mcp_registry line's registry; None for another line or a damaged one."""
|
|
82
|
+
parts = line.split("\t")
|
|
83
|
+
if len(parts) != 2 or parts[0] != MCP_REGISTRY_RECORD_TAG or not parts[1]:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return parse_registry(json.loads(base64.b64decode(parts[1], validate=True).decode("utf-8")))
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def evaluate_registry(registry, server, tool):
|
|
92
|
+
"""(code, rule_id, reason) refusing one MCP call, or None to let it continue.
|
|
93
|
+
|
|
94
|
+
A blocked server, then a held one, then (under deny) one not approved,
|
|
95
|
+
then a disabled tool.
|
|
96
|
+
"""
|
|
97
|
+
if server in registry["blockedServers"]:
|
|
98
|
+
return (
|
|
99
|
+
"SERVER_BLOCKED",
|
|
100
|
+
"mcp_registry." + server,
|
|
101
|
+
"MCP server \"" + server + "\" is blocked in this workspace's MCP server registry. "
|
|
102
|
+
"An owner or admin can change that on the MCP Servers page.",
|
|
103
|
+
)
|
|
104
|
+
if server in registry["heldServers"]:
|
|
105
|
+
return (
|
|
106
|
+
"SERVER_HELD",
|
|
107
|
+
"mcp_registry." + server,
|
|
108
|
+
"MCP server \"" + server + "\" changed its tools in a way scored high risk, and this workspace "
|
|
109
|
+
"holds such a server until it is approved again. It is waiting in the approval queue on the MCP "
|
|
110
|
+
"Servers page for an owner or admin.",
|
|
111
|
+
)
|
|
112
|
+
if registry["defaultPolicy"] == "deny" and server not in registry["approvedServers"]:
|
|
113
|
+
return (
|
|
114
|
+
"SERVER_NOT_APPROVED",
|
|
115
|
+
"mcpDefaultPolicy",
|
|
116
|
+
"MCP server \"" + server + "\" is not approved in this workspace's MCP server registry, "
|
|
117
|
+
"and the workspace refuses unapproved servers (mcpDefaultPolicy: deny). It is waiting in "
|
|
118
|
+
"the approval queue on the MCP Servers page for an owner or admin.",
|
|
119
|
+
)
|
|
120
|
+
if tool in registry["disabledTools"].get(server, []):
|
|
121
|
+
return (
|
|
122
|
+
"TOOL_DISABLED",
|
|
123
|
+
"mcp_registry." + server + "." + tool,
|
|
124
|
+
"Tool \"" + tool + "\" is disabled on MCP server \"" + server + "\" in this workspace's "
|
|
125
|
+
"MCP server registry. An owner or admin can re-enable it on the MCP Servers page.",
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def decode_allowlist_record(line):
|
|
131
|
+
"""The @mcp_allowlist line as {"severity", "servers"}; None for another line.
|
|
132
|
+
|
|
133
|
+
A severity other than shadow reads as block, so a damaged one refuses.
|
|
134
|
+
"""
|
|
135
|
+
parts = line.split("\t")
|
|
136
|
+
if len(parts) != 3 or parts[0] != MCP_ALLOWLIST_RECORD_TAG:
|
|
137
|
+
return None
|
|
138
|
+
return {
|
|
139
|
+
"severity": "shadow" if parts[1] == "shadow" else "block",
|
|
140
|
+
"servers": [s for s in parts[2].split(",") if s],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def evaluate_allowlist(allowlist, server):
|
|
145
|
+
"""(code, rule_id, reason) for a server the allowlist does not name, else None.
|
|
146
|
+
|
|
147
|
+
Under shadow the caller records the refusal instead of enforcing it.
|
|
148
|
+
"""
|
|
149
|
+
if server in allowlist["servers"]:
|
|
150
|
+
return None
|
|
151
|
+
return (
|
|
152
|
+
"SERVER_NOT_ALLOWED",
|
|
153
|
+
"mcp_allowlist",
|
|
154
|
+
"MCP server \"" + server + "\" is not on the MCP server allowlist for this workspace",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def unverified_refusal(server):
|
|
159
|
+
"""(code, rule_id, reason) refusing an MCP call on an unverified snapshot.
|
|
160
|
+
|
|
161
|
+
Neither the registry's approvals nor the allowlist's servers can be
|
|
162
|
+
vouched for, and a deleted record looks like one never set, so the
|
|
163
|
+
snapshot admits no MCP server at all.
|
|
164
|
+
"""
|
|
165
|
+
return (
|
|
166
|
+
"POLICY_SNAPSHOT_UNVERIFIED",
|
|
167
|
+
"policy_snapshot",
|
|
168
|
+
"MCP server \"" + server + "\" is refused because the policy snapshot on this machine failed its "
|
|
169
|
+
"integrity check, so its MCP server registry and allowlist admit no server until the sync daemon "
|
|
170
|
+
"restores a verified snapshot",
|
|
171
|
+
)
|
|
172
|
+
`;
|
|
173
|
+
//# sourceMappingURL=mcpRegistryPy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpRegistryPy.js","sourceRoot":"","sources":["../../src/lib/mcpRegistryPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkK/C,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The phrase matcher in Python, for the gates that run Python: the bash gates
|
|
3
|
+
* (their extractor already requires python3) and the Open WebUI filter.
|
|
4
|
+
*
|
|
5
|
+
* Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/phrases.py`,
|
|
6
|
+
* which a test compares; edit that file and copy it here. It is the
|
|
7
|
+
* transliteration of `@intutic/shared-types`'s phrases.ts, and both run the
|
|
8
|
+
* shared vectors.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PHRASES_PY_SOURCE: string;
|
|
11
|
+
//# sourceMappingURL=phrasesPy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phrasesPy.d.ts","sourceRoot":"","sources":["../../src/lib/phrasesPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,QAoV7B,CAAA"}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The phrase matcher in Python, for the gates that run Python: the bash gates
|
|
3
|
+
* (their extractor already requires python3) and the Open WebUI filter.
|
|
4
|
+
*
|
|
5
|
+
* Byte-identical to `packages/intutic-clawde/intutic_clawde/gate/phrases.py`,
|
|
6
|
+
* which a test compares; edit that file and copy it here. It is the
|
|
7
|
+
* transliteration of `@intutic/shared-types`'s phrases.ts, and both run the
|
|
8
|
+
* shared vectors.
|
|
9
|
+
*/
|
|
10
|
+
export const PHRASES_PY_SOURCE = String.raw `"""Phrase matching for shell commands and SQL in linear time.
|
|
11
|
+
|
|
12
|
+
A transliteration of packages/shared-types/src/phrases.ts; read that module's
|
|
13
|
+
docstring for what counts as a separator and why no regex searches the input.
|
|
14
|
+
The rules are the same; the walk is organised for CPython, which is slow per
|
|
15
|
+
step: runs of ordinary characters, spaces and backslashes are consumed by
|
|
16
|
+
anchored character-class matches (one pass each, nothing to backtrack into),
|
|
17
|
+
and a phrase is checked only at the words that can start it.
|
|
18
|
+
|
|
19
|
+
Every classifier runs the shared vectors in
|
|
20
|
+
packages/proxy/src/plugins/anomaly/action_vectors.json, and the hook gates
|
|
21
|
+
emit this file verbatim (services/sync-daemon/src/lib/phrasesPy.ts holds a
|
|
22
|
+
byte-identical copy, which a test compares). It must stay self-contained: the
|
|
23
|
+
standard library only, no module state beyond the constants below, and no
|
|
24
|
+
backtick or dollar-brace, which the TypeScript copy cannot hold.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from bisect import bisect_left
|
|
29
|
+
|
|
30
|
+
_SPACE_CHARS = (
|
|
31
|
+
" \t\n\r\v\f\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007"
|
|
32
|
+
"\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
33
|
+
)
|
|
34
|
+
_SPACES = frozenset(_SPACE_CHARS)
|
|
35
|
+
_WORD = frozenset("abcdefghijklmnopqrstuvwxyz0123456789_")
|
|
36
|
+
# One anchored match per piece of the text. Every alternative starts with a
|
|
37
|
+
# character no other alternative can start with, so exactly one can match at
|
|
38
|
+
# a position and it matches one run of its class: nothing to backtrack into.
|
|
39
|
+
_PIECE = re.compile(
|
|
40
|
+
"(?P<plain>[^" + _SPACE_CHARS + "\\\\/;&|-]+)"
|
|
41
|
+
"|(?P<space>[" + _SPACE_CHARS + "]+)"
|
|
42
|
+
"|(?P<dash>-+)"
|
|
43
|
+
"|(?P<backslash>\\\\+)"
|
|
44
|
+
"|(?P<other>[/;&|])"
|
|
45
|
+
)
|
|
46
|
+
# A run of words that need no decision, each followed by one plain space:
|
|
47
|
+
# ordinary characters, after at most a leading run of dashes. Consumed in one
|
|
48
|
+
# match and split in C — most of a long command is this.
|
|
49
|
+
_BULK = re.compile("(?:(?:[^" + _SPACE_CHARS + "\\\\/;&|-]+|-+[^" + _SPACE_CHARS + "\\\\/;&|-]*) )+")
|
|
50
|
+
_BULK_NO_COMMENT = re.compile("(?:(?:[^" + _SPACE_CHARS + "\\\\;&|-]+|-+[^" + _SPACE_CHARS + "\\\\;&|-]*) )+")
|
|
51
|
+
# A run of line continuations or escaped spaces between words: one break.
|
|
52
|
+
_CONTINUATIONS = re.compile("(?:\\\\[" + _SPACE_CHARS + "])+")
|
|
53
|
+
# The same once no "*/" is left in the text: no "/*" can open a comment, so a
|
|
54
|
+
# slash is an ordinary character.
|
|
55
|
+
_PIECE_NO_COMMENT = re.compile(
|
|
56
|
+
"(?P<plain>[^" + _SPACE_CHARS + "\\\\;&|-]+)"
|
|
57
|
+
"|(?P<space>[" + _SPACE_CHARS + "]+)"
|
|
58
|
+
"|(?P<dash>-+)"
|
|
59
|
+
"|(?P<backslash>\\\\+)"
|
|
60
|
+
"|(?P<other>[;&|])"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PhraseText:
|
|
65
|
+
"""A text cut into words, ready for any number of has_phrase calls."""
|
|
66
|
+
|
|
67
|
+
__slots__ = ("raw", "joined", "toks", "sep", "nl", "eol", "dash", "bar", "tail", "memo")
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.raw = ""
|
|
71
|
+
self.joined = ""
|
|
72
|
+
self.toks = []
|
|
73
|
+
self.sep = []
|
|
74
|
+
self.nl = []
|
|
75
|
+
self.eol = []
|
|
76
|
+
self.dash = []
|
|
77
|
+
self.bar = []
|
|
78
|
+
self.tail = False
|
|
79
|
+
self.memo = {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def phrase_text(value):
|
|
83
|
+
"""Cut value into words in one pass (see phraseText in phrases.ts)."""
|
|
84
|
+
s = ("" if value is None else str(value)).lower()
|
|
85
|
+
n = len(s)
|
|
86
|
+
t = PhraseText()
|
|
87
|
+
t.raw = s
|
|
88
|
+
toks, sep, nl, eol, dash, bar = t.toks, t.sep, t.nl, t.eol, t.dash, t.bar
|
|
89
|
+
start = -1
|
|
90
|
+
tok_bar = False
|
|
91
|
+
pend_sep = pend_nl = pend_eol = False
|
|
92
|
+
close = -2 if "*/" in s else -1 # the next "*/" at or after the last search; -1 once none is left
|
|
93
|
+
piece = _PIECE if close != -1 else _PIECE_NO_COMMENT
|
|
94
|
+
bulk = _BULK if close != -1 else _BULK_NO_COMMENT
|
|
95
|
+
i = 0
|
|
96
|
+
while i < n:
|
|
97
|
+
if start < 0:
|
|
98
|
+
m = bulk.match(s, i)
|
|
99
|
+
if m is not None:
|
|
100
|
+
words = m.group().split(" ")
|
|
101
|
+
words.pop()
|
|
102
|
+
rest = len(words) - 1
|
|
103
|
+
toks.extend(words)
|
|
104
|
+
sep.append(pend_sep)
|
|
105
|
+
sep.extend([True] * rest)
|
|
106
|
+
nl.append(pend_nl)
|
|
107
|
+
nl.extend([False] * rest)
|
|
108
|
+
eol.append(pend_eol)
|
|
109
|
+
eol.extend([False] * rest)
|
|
110
|
+
dash.extend([w.startswith("--") for w in words])
|
|
111
|
+
bar.extend([False] * (rest + 1))
|
|
112
|
+
pend_sep = True
|
|
113
|
+
pend_nl = pend_eol = False
|
|
114
|
+
i = m.end()
|
|
115
|
+
continue
|
|
116
|
+
m = _CONTINUATIONS.match(s, i)
|
|
117
|
+
if m is not None:
|
|
118
|
+
pend_sep = True
|
|
119
|
+
i = m.end()
|
|
120
|
+
continue
|
|
121
|
+
m = piece.match(s, i)
|
|
122
|
+
kind = m.lastgroup
|
|
123
|
+
j = m.end()
|
|
124
|
+
if kind == "plain":
|
|
125
|
+
if start < 0:
|
|
126
|
+
start = i
|
|
127
|
+
i = j
|
|
128
|
+
continue
|
|
129
|
+
word_break = False
|
|
130
|
+
newline = eol_here = False
|
|
131
|
+
nxt = j
|
|
132
|
+
if kind == "space":
|
|
133
|
+
newline = eol_here = "\n" in m.group()
|
|
134
|
+
word_break = True
|
|
135
|
+
elif kind == "dash":
|
|
136
|
+
# A "--" inside a word starts a new one, glued to it: it can open a
|
|
137
|
+
# comment or an option run but cannot stand for a plain gap.
|
|
138
|
+
if j - i > 1 and start >= 0:
|
|
139
|
+
toks.append(s[start:i])
|
|
140
|
+
sep.append(pend_sep)
|
|
141
|
+
nl.append(pend_nl)
|
|
142
|
+
eol.append(pend_eol)
|
|
143
|
+
dash.append(s.startswith("--", start))
|
|
144
|
+
bar.append(tok_bar)
|
|
145
|
+
start = -1
|
|
146
|
+
tok_bar = False
|
|
147
|
+
pend_sep = pend_nl = pend_eol = False
|
|
148
|
+
if start < 0:
|
|
149
|
+
start = i
|
|
150
|
+
i = j
|
|
151
|
+
continue
|
|
152
|
+
elif kind == "backslash":
|
|
153
|
+
# Every backslash but the last is followed by a backslash: a
|
|
154
|
+
# literal character. The last one decides.
|
|
155
|
+
last = j - 1
|
|
156
|
+
d = s[j] if j < n else ""
|
|
157
|
+
if d in ("n", "t", "r") or (d != "" and d in _SPACES):
|
|
158
|
+
if last > i and start < 0:
|
|
159
|
+
start = i
|
|
160
|
+
word_break = True
|
|
161
|
+
eol_here = d == "n"
|
|
162
|
+
nxt = j + 1
|
|
163
|
+
if last > i:
|
|
164
|
+
# the literal backslashes end the word before the escape
|
|
165
|
+
toks.append(s[start:last])
|
|
166
|
+
sep.append(pend_sep)
|
|
167
|
+
nl.append(pend_nl)
|
|
168
|
+
eol.append(pend_eol)
|
|
169
|
+
dash.append(s.startswith("--", start))
|
|
170
|
+
bar.append(tok_bar)
|
|
171
|
+
start = -1
|
|
172
|
+
tok_bar = False
|
|
173
|
+
pend_sep = pend_nl = pend_eol = False
|
|
174
|
+
else:
|
|
175
|
+
if start < 0:
|
|
176
|
+
start = i
|
|
177
|
+
i = j
|
|
178
|
+
continue
|
|
179
|
+
else:
|
|
180
|
+
c = s[i]
|
|
181
|
+
if c == "/" and j < n and s[j] == "*" and close != -1:
|
|
182
|
+
if close < i + 2:
|
|
183
|
+
close = s.find("*/", i + 2)
|
|
184
|
+
if close == -1:
|
|
185
|
+
piece = _PIECE_NO_COMMENT
|
|
186
|
+
bulk = _BULK_NO_COMMENT
|
|
187
|
+
if close != -1:
|
|
188
|
+
word_break = True
|
|
189
|
+
nxt = close + 2
|
|
190
|
+
if not word_break:
|
|
191
|
+
if start < 0:
|
|
192
|
+
start = i
|
|
193
|
+
if c != "/":
|
|
194
|
+
tok_bar = True
|
|
195
|
+
i = j
|
|
196
|
+
continue
|
|
197
|
+
if start >= 0:
|
|
198
|
+
toks.append(s[start:i])
|
|
199
|
+
sep.append(pend_sep)
|
|
200
|
+
nl.append(pend_nl)
|
|
201
|
+
eol.append(pend_eol)
|
|
202
|
+
dash.append(s.startswith("--", start))
|
|
203
|
+
bar.append(tok_bar)
|
|
204
|
+
start = -1
|
|
205
|
+
tok_bar = False
|
|
206
|
+
pend_sep = pend_nl = pend_eol = False
|
|
207
|
+
pend_sep = True
|
|
208
|
+
if newline:
|
|
209
|
+
pend_nl = True
|
|
210
|
+
if eol_here:
|
|
211
|
+
pend_eol = True
|
|
212
|
+
i = nxt
|
|
213
|
+
if start >= 0:
|
|
214
|
+
toks.append(s[start:n])
|
|
215
|
+
sep.append(pend_sep)
|
|
216
|
+
nl.append(pend_nl)
|
|
217
|
+
eol.append(pend_eol)
|
|
218
|
+
dash.append(s.startswith("--", start))
|
|
219
|
+
bar.append(tok_bar)
|
|
220
|
+
pend_sep = False
|
|
221
|
+
t.tail = pend_sep
|
|
222
|
+
t.joined = " ".join(toks)
|
|
223
|
+
return t
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _next_index(t):
|
|
227
|
+
"""Per word x: the first word at or after x containing ;&|, with a real
|
|
228
|
+
newline before it, and with any newline before it. Built once per text."""
|
|
229
|
+
ix = t.memo.get("#next")
|
|
230
|
+
if ix is None:
|
|
231
|
+
T = len(t.toks)
|
|
232
|
+
nb = [T] * (T + 1)
|
|
233
|
+
nn = [T] * (T + 1)
|
|
234
|
+
ne = [T] * (T + 1)
|
|
235
|
+
b = n_ = e = T
|
|
236
|
+
bar, nl, eol = t.bar, t.nl, t.eol
|
|
237
|
+
for x in range(T - 1, -1, -1):
|
|
238
|
+
if bar[x]:
|
|
239
|
+
b = x
|
|
240
|
+
if nl[x]:
|
|
241
|
+
n_ = x
|
|
242
|
+
if eol[x]:
|
|
243
|
+
e = x
|
|
244
|
+
nb[x] = b
|
|
245
|
+
nn[x] = n_
|
|
246
|
+
ne[x] = e
|
|
247
|
+
ix = (nb, nn, ne)
|
|
248
|
+
t.memo["#next"] = ix
|
|
249
|
+
return ix
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def has_phrase(t, needle, bounded=False):
|
|
253
|
+
"""Whether t contains needle (see hasPhrase in phrases.ts)."""
|
|
254
|
+
words = needle.lower().split(" ")
|
|
255
|
+
if len(words) == 1:
|
|
256
|
+
return words[0] in t.raw
|
|
257
|
+
trailing_gap = words[-1] == ""
|
|
258
|
+
if trailing_gap:
|
|
259
|
+
words.pop()
|
|
260
|
+
for w in words:
|
|
261
|
+
if w == "" or w not in t.joined:
|
|
262
|
+
return False
|
|
263
|
+
toks, sep, dash, tail = t.toks, t.sep, t.dash, t.tail
|
|
264
|
+
T = len(toks)
|
|
265
|
+
k = len(words)
|
|
266
|
+
|
|
267
|
+
def positions(i):
|
|
268
|
+
"""The words that word i of the phrase can be, by its position."""
|
|
269
|
+
w = words[i]
|
|
270
|
+
first = i == 0
|
|
271
|
+
last = i == k - 1 and not trailing_gap
|
|
272
|
+
key = ("f" if first else "") + ("l" if last else "") + ("b" if bounded else "") + ":" + w
|
|
273
|
+
hit = t.memo.get(key)
|
|
274
|
+
if hit is not None:
|
|
275
|
+
return hit
|
|
276
|
+
lw = len(w)
|
|
277
|
+
if first:
|
|
278
|
+
out = [q for q, tok in enumerate(toks) if tok.endswith(w)]
|
|
279
|
+
elif last:
|
|
280
|
+
out = [q for q, tok in enumerate(toks) if tok.startswith(w)]
|
|
281
|
+
else:
|
|
282
|
+
out = [q for q, tok in enumerate(toks) if tok == w]
|
|
283
|
+
if bounded:
|
|
284
|
+
kept = []
|
|
285
|
+
for q in out:
|
|
286
|
+
tok = toks[q]
|
|
287
|
+
if first and len(tok) > lw and tok[len(tok) - lw - 1] in _WORD:
|
|
288
|
+
continue
|
|
289
|
+
if last and len(tok) > lw and (tok[lw] in _WORD or tok[lw] == "."):
|
|
290
|
+
continue
|
|
291
|
+
kept.append(q)
|
|
292
|
+
out = kept
|
|
293
|
+
t.memo[key] = out
|
|
294
|
+
return out
|
|
295
|
+
|
|
296
|
+
# The words at which the rest of the phrase matches, ascending.
|
|
297
|
+
ok = positions(k - 1)
|
|
298
|
+
if trailing_gap:
|
|
299
|
+
kept = []
|
|
300
|
+
for q in ok:
|
|
301
|
+
r = q + 1
|
|
302
|
+
if r < T:
|
|
303
|
+
if sep[r] or (dash[r] and not t.bar[r] and (r + 1 < T or tail)):
|
|
304
|
+
kept.append(q)
|
|
305
|
+
elif tail:
|
|
306
|
+
kept.append(q)
|
|
307
|
+
ok = kept
|
|
308
|
+
if k == 1:
|
|
309
|
+
return bool(ok)
|
|
310
|
+
nb, nn, ne = _next_index(t)
|
|
311
|
+
for i in range(k - 2, -1, -1):
|
|
312
|
+
ok_sep = [q for q in ok if sep[q]]
|
|
313
|
+
ok_set = set(ok_sep)
|
|
314
|
+
memo = {}
|
|
315
|
+
|
|
316
|
+
def option(r):
|
|
317
|
+
# An option run reaches any word before a ;&| word or a real newline.
|
|
318
|
+
j = bisect_left(ok_sep, r + 1)
|
|
319
|
+
return j < len(ok_sep) and ok_sep[j] <= min(nb[r], nn[r + 1], T - 1)
|
|
320
|
+
|
|
321
|
+
def reach(r):
|
|
322
|
+
# The gap that starts at word r leads into the rest of the phrase:
|
|
323
|
+
# directly, after an option run, or after -- comments, each of
|
|
324
|
+
# which runs to the next newline.
|
|
325
|
+
path = []
|
|
326
|
+
cur = r
|
|
327
|
+
while True:
|
|
328
|
+
if cur >= T:
|
|
329
|
+
val = False
|
|
330
|
+
break
|
|
331
|
+
if cur in memo:
|
|
332
|
+
val = memo[cur]
|
|
333
|
+
break
|
|
334
|
+
if cur in ok_set or (dash[cur] and option(cur)):
|
|
335
|
+
val = memo[cur] = True
|
|
336
|
+
break
|
|
337
|
+
if not dash[cur]:
|
|
338
|
+
val = memo[cur] = False
|
|
339
|
+
break
|
|
340
|
+
path.append(cur)
|
|
341
|
+
cur = ne[cur + 1]
|
|
342
|
+
for x in path:
|
|
343
|
+
memo[x] = val
|
|
344
|
+
return val
|
|
345
|
+
|
|
346
|
+
ok = [p for p in positions(i) if reach(p + 1)]
|
|
347
|
+
if not ok:
|
|
348
|
+
return False
|
|
349
|
+
return bool(ok)
|
|
350
|
+
`;
|
|
351
|
+
//# sourceMappingURL=phrasesPy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phrasesPy.js","sourceRoot":"","sources":["../../src/lib/phrasesPy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoV1C,CAAA"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* # Why
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* `POST /api/v1/hook-gate` was called by two of thirteen gates, so DLP,
|
|
8
8
|
* `BLOCK:` SOPs, SSO policy and promoted findings reached two of thirteen. The
|
|
9
9
|
* obvious repair — route the other eleven through the gate — is the wrong one:
|
|
10
10
|
* those eleven enforce locally and **fail closed**, the two network callers were
|
|
@@ -54,13 +54,28 @@
|
|
|
54
54
|
*
|
|
55
55
|
* @module
|
|
56
56
|
*/
|
|
57
|
-
import { type SsoGroupPolicy } from '@intutic/shared-types';
|
|
57
|
+
import { type McpRegistryRecord, type SsoGroupPolicy } from '@intutic/shared-types';
|
|
58
58
|
import { type GuardPattern } from '../harness/protectedPaths.js';
|
|
59
59
|
/** Directory every gate looks in. Inside `.intutic/hooks`, which is already
|
|
60
60
|
* protected — see the module note. */
|
|
61
61
|
export declare const DEFAULT_SNAPSHOT_DIR: string;
|
|
62
62
|
export declare const SNAPSHOT_JSON = "policy-snapshot.json";
|
|
63
63
|
export declare const SNAPSHOT_RULES = "policy-snapshot.rules";
|
|
64
|
+
/**
|
|
65
|
+
* The subdirectory of the snapshot directory holding the last snapshot the
|
|
66
|
+
* daemon wrote and so verified: both artifacts, byte for byte, read-only.
|
|
67
|
+
* Inside `.intutic/hooks`, which every gate refuses to touch. The settings
|
|
68
|
+
* guard compares the live snapshot with it and restores it when they differ
|
|
69
|
+
* ({@link restoreVerifiedSnapshot}), the way a policy agent keeps its last
|
|
70
|
+
* verified bundle active when a new one fails verification.
|
|
71
|
+
*/
|
|
72
|
+
export declare const VERIFIED_SNAPSHOT_DIR = "verified";
|
|
73
|
+
/**
|
|
74
|
+
* Whether a `.rules` text verifies: it carries a `#digest` line, and the
|
|
75
|
+
* digest of its data lines matches it — the check every gate makes. A
|
|
76
|
+
* missing digest fails, since the writer always writes one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function snapshotRulesVerify(text: string): boolean;
|
|
64
79
|
/**
|
|
65
80
|
* Whether the destructive tier ships as `block` or as `warn`.
|
|
66
81
|
*
|
|
@@ -94,10 +109,9 @@ export declare const DESTRUCTIVE_TIER_SEVERITY: 'block' | 'warn';
|
|
|
94
109
|
* is not that kind of rule: it does not run `scanSkillContent` or judge
|
|
95
110
|
* anything about what a skill file says, only WHERE a write's target path
|
|
96
111
|
* points — `.agents/skills/**` or `.claude/skills/**`, a deterministic,
|
|
97
|
-
* zero-ambiguity match.
|
|
98
|
-
* that distinction: "Path-matching is the one exception, and it is not
|
|
112
|
+
* zero-ambiguity match. Path-matching is the one exception, and it is not
|
|
99
113
|
* actually an exception to the measurement requirement — it never needed
|
|
100
|
-
* one.
|
|
114
|
+
* one. There is no false-positive rate to earn a promotion by measuring,
|
|
101
115
|
* because there is nothing probabilistic here to measure in the first place.
|
|
102
116
|
*
|
|
103
117
|
* The second half of the argument is what `warn` actually buys on this
|
|
@@ -132,7 +146,7 @@ export declare const SKILL_SURFACE_TIER_SEVERITY: 'block' | 'warn';
|
|
|
132
146
|
* ships as `block` or as `warn`.
|
|
133
147
|
*
|
|
134
148
|
* `block`, licensed by a measurement rather than by argument, unlike
|
|
135
|
-
* `SKILL_SURFACE_TIER_SEVERITY` above.
|
|
149
|
+
* `SKILL_SURFACE_TIER_SEVERITY` above. Content enforcement was held at warn
|
|
136
150
|
* until `scanSkillContent`'s false-positive rate on real benign skills had been
|
|
137
151
|
* measured. It now has: 350 vendored SKILL.md files
|
|
138
152
|
* (`packages/shared-types/src/__tests__/corpus/skills/`), and the nine patterns
|
|
@@ -160,7 +174,7 @@ export interface ResolvedPolicy {
|
|
|
160
174
|
action: string;
|
|
161
175
|
reason: string;
|
|
162
176
|
/**
|
|
163
|
-
*
|
|
177
|
+
* `'guardrail'` on rules the control plane projects from a cited
|
|
164
178
|
* policy guardrail. The only origin for which `action: 'warn'` is shipped
|
|
165
179
|
* (at severity `warn`, report-only); every other warn rule is still
|
|
166
180
|
* dropped, the HIGH/CRITICAL catch-all included.
|
|
@@ -175,9 +189,9 @@ export interface ResolvedPolicy {
|
|
|
175
189
|
* this same field name off `workspaces.settings`, so this is not a new
|
|
176
190
|
* name invented for the snapshot). Absent/empty means unrestricted — the
|
|
177
191
|
* MCP proxy already reads it that way (`packages/mcp-proxy/src/policy.ts`),
|
|
178
|
-
* and the gate-side
|
|
192
|
+
* and the gate-side `@mcp_allowlist` record this field feeds
|
|
179
193
|
* (`writePolicySnapshot` below) preserves the same convention: an empty
|
|
180
|
-
* list
|
|
194
|
+
* list writes no record rather than a deny-everything one.
|
|
181
195
|
*/
|
|
182
196
|
mcpAllowedServers: string[];
|
|
183
197
|
/**
|
|
@@ -203,6 +217,14 @@ export interface ResolvedPolicy {
|
|
|
203
217
|
memberId: string;
|
|
204
218
|
ssoGroups: string[];
|
|
205
219
|
} | null;
|
|
220
|
+
/**
|
|
221
|
+
* The workspace's MCP server registry decisions (`mcpRegistry`): blocked,
|
|
222
|
+
* held and approved servers, disabled tools and `mcpDefaultPolicy`. Written
|
|
223
|
+
* as an `@mcp_registry` record so the hook gates apply them to every
|
|
224
|
+
* `mcp__<server>__<tool>` call, not only the calls an MCP proxy fronts.
|
|
225
|
+
* Absent or null: an older control plane, and no record is written.
|
|
226
|
+
*/
|
|
227
|
+
mcpRegistry?: McpRegistryRecord | null;
|
|
206
228
|
}
|
|
207
229
|
export interface PolicySnapshotOptions {
|
|
208
230
|
controlPlaneUrl: string;
|
|
@@ -214,7 +236,7 @@ export interface PolicySnapshotOptions {
|
|
|
214
236
|
* `review_before:` tokens from the workspace's local SOPs and settings
|
|
215
237
|
* (`parseSopConstraints(...).reviewBefore`). Each becomes a `hold` rule,
|
|
216
238
|
* `sop.local.review_before.<token>`, so every gate holds on them — not only
|
|
217
|
-
* the Claude Code hook that used to bake them in
|
|
239
|
+
* the Claude Code hook that used to bake them in.
|
|
218
240
|
*/
|
|
219
241
|
localHoldTokens?: readonly string[];
|
|
220
242
|
}
|
|
@@ -244,6 +266,20 @@ export declare function writePolicySnapshot(policy: ResolvedPolicy, snapshotDir?
|
|
|
244
266
|
digest: string;
|
|
245
267
|
ruleCount: number;
|
|
246
268
|
}>;
|
|
269
|
+
/**
|
|
270
|
+
* Puts the last verified snapshot back when the live one differs from it.
|
|
271
|
+
*
|
|
272
|
+
* `'intact'`: the live snapshot is the verified one, or there is nothing to
|
|
273
|
+
* compare (no snapshot written yet, or a live one from before the verified
|
|
274
|
+
* copy existed that still verifies). `'restored'`: either live artifact was
|
|
275
|
+
* edited, replaced or deleted, and both are the verified copy again.
|
|
276
|
+
* `'unverifiable'`: the live `.rules` fails its digest and there is no
|
|
277
|
+
* verified copy to restore, so the caller fetches a fresh snapshot. Any
|
|
278
|
+
* difference counts, a valid-looking one included: the daemon is the only
|
|
279
|
+
* writer of these files, so an older snapshot copied back in is tampering
|
|
280
|
+
* too. Never throws.
|
|
281
|
+
*/
|
|
282
|
+
export declare function restoreVerifiedSnapshot(snapshotDir?: string): Promise<'intact' | 'restored' | 'unverifiable'>;
|
|
247
283
|
/**
|
|
248
284
|
* Fetch and write in one call. Safe to run on every sync cycle.
|
|
249
285
|
*
|